If you want to contribute, please, create an issue or PR on the main project https://github.com/fscpscollaborative/fscps
Test-FinOpsNuGetAccess.ps1 checks the Developer
Tools Service used by the Power Platform Tools Visual Studio extension. It lists
releases, selects the latest application build (or an explicit -AppBuild), obtains
links for all five build packages, and checks each blob with HEAD. It does not
download packages, upload anything, or change the existing update workflow.
Automatic selection is a connectivity check, not a recommendation to upgrade.
Run with PowerShell 7 and an Azure CLI session in the environment's tenant:
az login --tenant <tenant-id> --allow-no-subscriptions
.\Actions\Helpers\Test-FinOpsNuGetAccess.ps1 -DataverseUrl https://contoso.crm.dynamics.com/The script obtains a token for the Dataverse organization, not for LCS or the
Developer Tools Service hostname. Alternatively, pass -AccessToken as a
SecureString obtained by your chosen authentication method. Never paste tokens,
passwords, SAS links, or client secrets into chat or workflow logs.
An application-token probe can use an existing service-principal Azure CLI session. Do not replace an interactive profile or create credentials without approval. Application-user access in Dataverse must already be configured. Support for application tokens by this particular service is not yet verified; successful user-token access does not prove unattended CI access.
These are internal API contracts, not a documented compatibility guarantee. Only public-cloud Dataverse and Azure Blob URLs are currently accepted. Errors report HTTP status and correlation ID without response bodies or SAS URLs. A successful probe verifies link issuance and blob accessibility, not NuGet archive integrity or completeness of the legacy SDP/version-update workflow.
Run the isolated tests with Pester 5:
Invoke-Pester -Path .\Actions\Helpers\Test-FinOpsNuGetAccess.Tests.ps1Update NuGets (Dataverse) is a separate, opt-in replacement for the NuGet part of the LCS workflow. The legacy workflow remains unchanged because Software Deployable Packages and LCS asset IDs are not available from this API. Do not run both writers concurrently; disable the legacy schedule after migrating, or run its SDP updates separately.
The new workflow discovers every advertised release and, by default, synchronizes
the newest available application build of each release. all_builds explicitly
backfills every build currently returned by the service; this can transfer hundreds
of GB and may require multiple runs. Daily latest-only synchronization does not
archive builds that appear and disappear between runs.
Each package is downloaded to a unique temporary directory, checked against its
root .nuspec ID/version and Content-Length, hashed with SHA-256, and uploaded to
the existing flat layout <PackageId>.<NuGetVersion>.nupkg. Platform packages use
the platform version; application packages use the application version. Packages
are removed locally after each upload. The publisher sets SHA-256/version blob
metadata, verifies the published size and metadata, and skips matching packages
on subsequent runs. Legacy blobs without validation metadata are downloaded and
republished once. The container must already exist.
The build tooling (Get-FSCPSNuget -Type ApplicationDevALM, and the packages.config
template) still requests Microsoft.Dynamics.AX.Application.DevALM.BuildXpp, a single
package that the Developer Tools API no longer lists: it only offers Application1 and
Application2. After publishing the five listed packages, the script therefore also
publishes the combined package for each build, assembled from those two parts by
Merge-FinOpsApplicationPackage. On November 6, 2026 this was validated against
Microsoft's own combined package for 10.0.49 (build 10.0.2790.113): the merged archive had
the same 15,018 files with identical sizes, and identical nuspec and .rels text. The
only difference is that the NuGet signature (.signature.p7s) is dropped, because a
signature covers the original archive and would be invalid after merging. The result is
therefore unsigned; NuGet accepts unsigned packages under its default
signatureValidationMode=accept, so do not set it to require for these sources. An
existing combined blob (for example one published by Microsoft's pipeline) is never
replaced, and the versions file is advanced only after the combined package exists, so a
build that follows AppVersionLatest always finds all four packages it needs.
Only after all selected packages are published does the script atomically update
versions.default.json. It adds new release records,
advances AppVersionLatest and PlatformVersionLatest without downgrading, and
sets PlatformUpdate. Existing GA versions, LCS IDs, Commerce branch and custom
fields are preserved. New records have empty GA and LCS fields: the API does not
identify GA or provide SDP IDs. No partially updated JSON is committed on failure;
successful blob uploads remain available and can be reused on rerun.
-
Register an Entra application and add its application user to the target Dataverse organization with appropriate access. This service's application token support is still unverified; first run
list_onlyusing that identity. -
Configure a federated identity credential with issuer
https://token.actions.githubusercontent.com, audienceapi://AzureADTokenExchange, and subjectrepo:<owner>/<repository>:ref:refs/heads/<branch>. The subject must match the repository and branch actually running the workflow (including its default branch for schedules). No client secret or user refresh token is needed. -
Configure these GitHub repository variables:
Variable Value FINOPS_DATAVERSE_URLOrganization root URL, e.g. https://orgf60be27e.crm4.dynamics.com/FINOPS_AZURE_TENANT_IDDataverse tenant ID FINOPS_AZURE_CLIENT_IDApplication/client ID FINOPS_NUGET_STORAGE_ACCOUNTOptional; defaults to ciellosarchiveFINOPS_NUGET_STORAGE_CONTAINEROptional; defaults to nugetFINOPS_NUGET_SYNC_ENABLEDSet to trueonly after a successful manual synchronization; enables the daily 22:00 UTC runFINOPS_BUILD_PREFIX_OVERRIDESOptional JSON object; defaults to {"10.0.45":"10.0.2345"}FINOPS_ADDITIONAL_RELEASESOptional JSON array of releases the service does not list yet; defaults to [{"appVersion":"10.0.50","appBuild":"10.0.2919","platVersion":"PU74"}]FINOPS_DISCOVER_NEXT_RELEASEOptional; defaults to true. Probes build prefixes above the newest known release (about 400 requests, ~30 s) to find a new preview that the catalog does not list yetPreview releases: the service catalog (
finopsversions) may omit a release whose builds are already downloadable. For 10.0.50 (PU74, build10.0.2919, platform7.0.8358) the catalog omitted it on November 5, 2026, yetfinopsbuilds,finopsplatbuildandnugetpackageall worked. Releases listed inFINOPS_ADDITIONAL_RELEASESare processed with a warning; once the catalog advertises the release the catalog entry wins. Take the build number from Microsoft's "Platform updates for version X" article. 10.0.50 packages target Visual Studio 2026 and are preview builds.With
FINOPS_DISCOVER_NEXT_RELEASE=truethe next release (for example 10.0.51, PU75) is also found automatically: the script probes the next 400 build prefixes after the newest known release and labels the first one with builds as the following version and platform update. This assumes consecutive release numbering. The result is ignored (with a warning) if any probe below the hit failed, and nothing is added when no prefix has builds. If a release ever skips a number or the build gap exceeds 400, add it throughFINOPS_ADDITIONAL_RELEASES.The prefix override is explicit because the service advertises
10.0.2545for 10.0.45, whose build list is empty;10.0.2345was verified against the service. Set the variable to{}when Microsoft fixes the catalog. Unknown catalog or package inconsistencies fail instead of guessing a version. -
Storage authentication uses the existing
NUGET_STORAGE_SAS_TOKENsecret when present (read, create/write permissions are required). Otherwise grant the federated principal Storage Blob Data Contributor on the target container; the script uses Azure CLI--auth-mode login. Blob traffic uses the storage identity/SAS, never the Dataverse token. -
Dispatch with
list_only: truefirst (the default). Then dispatch withlist_only: falseto publish and commit JSON to the selected branch. The workflow stages only the versions file. Branch protection must permit the bot's push, otherwise publication succeeds but the commit push fails explicitly. Push conflicts are not force-resolved: rerun on the latest branch revision.
The schedule is disabled until explicitly enabled. list_only does not access
storage or write files. The GitHub job summary shows the selected releases/builds
or publication results, without tokens or download URLs. The existing access
probe can independently verify SAS issuance and blob HEAD access.
The Developer Tools API has no endpoint for Software Deployable Packages, so the
workflow's second step, Sync-LcsDeployablePackages.ps1,
still uses LCS. It calls the LCS API with an OAuth token obtained from the existing
LCS_CLIENT_ID, LCS_USERNAME, LCS_USER_PASSWORD and LCS_PROJECT_ID secrets. It
does not use the browser sign-in of the fscps.lcs module, which since
October 30, 2026 stops at Microsoft's "Let's keep your account secure" MFA
registration page (diagnosed in a runner with masked output). The token request still
works for this account, but it depends on the tenant continuing to allow it; failures
report only AADSTS codes.
The LCS API returns no platform-build metadata, so the script derives type and version
from the asset name (Service Update, Preview Version, Proactive Quality Update,
Final Quality Update, versions above 10.0.39) and treats the asset with the newest
CreatedDate in each role as current. It publishes each selected asset once to the
deployablepackages container under its LCS name (as the legacy pipeline did), with
blob metadata lcsassetid. Existing blobs without that metadata are kept only if
uploaded after the LCS asset was created. Downloads are size-checked against
Content-Length and the LCS asset size, and the JSON is updated only after all selected
packages are in storage. FSCServiseUpdatePackageId, FSCPreviewVersionPackageId,
FSCLatestQualityUpdatePackageId and FSCFinalQualityUpdatePackageId are updated; a
release that has no record yet is created with PlatformUpdate = minor + 24.
The step runs independently of NuGet synchronization: a failure in one does not stop the
other's results from being committed, but the run is then marked failed. Set the
repository variable FINOPS_SDP_SYNC_ENABLED=false to disable it, and
FINOPS_SDP_STORAGE_CONTAINER to change the container. The storage SAS token must also
permit this container. The first run may upload many gigabytes. The retired nightly
schedule of the legacy "Update NuGets" workflow was removed so only one pipeline
writes the versions file.
Application-token discovery and SAS issuance were successfully verified on
November 5, 2026 against https://operations-on-qa.crm.dynamics.com/, tenant
46e85934-1fab-4533-a4ad-de92ce1fd81a, application XPP-MCP (Client ID
ddd14b4b-a5fb-4491-84ea-d5bdabcb5d71). Its application user currently has
System Administrator for validation; reduce that role after verifying the
required permissions. The Ciellos details below describe the earlier setup.
Creating federation was denied by Entra. Until an administrator configures OIDC,
the workflow supports an explicitly selected FINOPS_AZURE_AUTH_MODE=client-secret
repository variable and a FINOPS_AZURE_CREDENTIALS repository secret containing
the Azure login JSON (clientId, clientSecret, tenantId, and optionally
subscriptionId). Upload it from a protected local file, never commit it or
print its contents. This is a temporary alternative, not a silent fallback after
OIDC failure. Rotate the secret according to organizational policy.
After federation is configured, set FINOPS_AZURE_AUTH_MODE=oidc, verify a full
run, then remove FINOPS_AZURE_CREDENTIALS if no longer needed. OIDC is the
default when the mode variable is absent. Scheduled synchronization remains
independently gated by FINOPS_NUGET_SYNC_ENABLED.
Client ID is the Application (client) ID of an Entra app registration. It is
not a user account, an application Object ID, an Enterprise Application Object
ID, or a secret. Do not assume that the existing LCS_CLIENT_ID has the required
Dataverse access. Prefer a dedicated single-tenant application for this job.
The target verified during investigation:
| Setting | Value |
|---|---|
| Dataverse URL | https://orgf60be27e.crm4.dynamics.com/ |
| Directory (tenant) ID, Ciellos | 79bb53e9-f531-46a9-8a2d-49084810b651 |
| Target GitHub repository | fscpscollaborative/fscps |
| Default branch for scheduled runs | develop (verified November 3, 2026) |
| Storage account / existing container | ciellosarchive / nuget |
The local checkout's origin is onikolaiev/fscps, not the target repository.
Federation must trust the repository actually executing the workflow. A credential
for the collaborative repository does not authorize a run in the personal fork.
Recheck the default branch before configuration.
Ask the Entra/Power Platform administrator to perform the following:
-
In Microsoft Entra admin center, select the Ciellos tenant, then App registrations > New registration. For example, name it
fscps-finops-nuget-sync, select accounts in this directory only, and leave Redirect URI empty. An existing dedicated application can be reused after reviewing its owners and permissions. -
On the application's Overview page, copy Application (client) ID and Directory (tenant) ID. These identifiers are not secrets and can be shared with the repository maintainer. No client secret is needed for OIDC.
-
In Power Platform admin center, go to Manage > Environments, select the environment matching the URL above, then Settings > Users + permissions > Application users > New app user. Choose Add an app, select the same registration by its Client ID, choose the appropriate business unit, and assign an administrator-approved security role before creating the user.
The minimum role required by this internal Developer Tools endpoint is not established. Do not claim that ordinary API permissions or an Azure RBAC role grant Dataverse access. If necessary, an administrator can approve temporary System Administrator access for the initial probe, then narrow the role and repeat the probe before enabling production. Do not grant tenant-wide Graph permissions or create a user password to solve this.
-
In the Entra app registration, open Certificates & secrets > Federated credentials > Add credential and choose GitHub Actions deploying Azure resources. Enter organization
fscpscollaborative, repositoryfscps, entity type Branch, branchdevelop, and a name such asgithub-fscps-develop. Verify the resulting values exactly:Issuer: https://token.actions.githubusercontent.com Subject: repo:fscpscollaborative/fscps:ref:refs/heads/develop Audience: api://AzureADTokenExchangeThis trusts all eligible workflows on that branch, not only one YAML file: protect branch write access accordingly. Runs on another branch need a separately approved credential. The current workflow does not use a GitHub Environment; if one is introduced, its OIDC subject changes and the credential must be updated.
-
For secretless storage access, have the Azure administrator assign Storage Blob Data Contributor to the same service principal at the scope of the existing
nugetcontainer, not the entire subscription. The Azure principal must exist in the storage subscription's tenant; cross-tenant storage access is a separate configuration, not implied by Dataverse access.If retaining the existing
NUGET_STORAGE_SAS_TOKEN, skip this RBAC change. The secret must remain in GitHub and must permit reading and creating/writing blobs, including setting metadata. The workflow prefers this SAS when present; switch away from it deliberately only after validating RBAC.
Ask the GitHub repository administrator/maintainer to:
-
Make the new workflow and helper scripts available on
developinfscpscollaborative/fscps. They are not deployed merely by editing this checkout. -
Open Settings > Secrets and variables > Actions > Variables and set:
FINOPS_DATAVERSE_URL = https://orgf60be27e.crm4.dynamics.com/ FINOPS_AZURE_TENANT_ID = 79bb53e9-f531-46a9-8a2d-49084810b651 FINOPS_AZURE_CLIENT_ID = <Application (client) ID supplied by the Entra admin>Leave
FINOPS_NUGET_SYNC_ENABLEDunset orfalse. Client ID and tenant ID belong in variables, not in client-secret/password fields. -
Run Actions > Update NuGets (Dataverse) > Run workflow on
develop, withlist_onlychecked. This validates GitHub token exchange and the application's access to release/build discovery; it does not validate storage or SAS issuance. -
If discovery succeeds, run with
list_onlyunchecked andall_buildsunchecked. Verify package publication and the versions-file commit before enabling the schedule. Application-token authorization for SAS issuance may still fail even when catalog discovery works. Capture HTTP status/correlation ID for the administrator; do not export bearer tokens or SAS links. -
Enable
FINOPS_NUGET_SYNC_ENABLED=trueonly after the full run succeeds and the legacy JSON writer is no longer scheduled concurrently.
The administrator should return the Client ID, tenant confirmation, application user/role confirmation, federated credential subject, and selected storage authentication mode. Do not request or share passwords, client secrets, user refresh tokens, access tokens, or SAS values. If application authentication is rejected by the Developer Tools service, stop and investigate with the correlation ID; OIDC configuration alone cannot make a user-only endpoint support applications.
Official configuration references:
For local discovery with an existing Azure CLI session:
.\Actions\Helpers\Sync-FinOpsNuGets.ps1 `
-DataverseUrl https://orgf60be27e.crm4.dynamics.com/ `
-BuildPrefixOverrides @{ '10.0.45' = '10.0.2345' } -ListOnlyRun all isolated tests:
Invoke-Pester -Path .\Actions\Helpers\Test-FinOpsNuGetAccess.Tests.ps1, .\Actions\Helpers\Sync-FinOpsNuGets.Tests.ps1