Powered by Zoomin Software. For more details please contactZoomin

Use Semaphore for SharePoint Online

Deploy PDC to SharePoint Online sites

  • Last Updated: May 1, 2026
  • 7 minute read
    • Semaphore
    • Documentation

This sample PowerShell script automates the deployment of Progress Data Cloud to multiple SharePoint Online sites. It is an alternative to the manual deployment process described in Install Semaphore for SharePoint Online.

Script overview

The Deploy-PdcToSpo.ps1 script reads a list of SharePoint site URLs from a text file and, for each site that belongs to a configured subscription, performs the following actions:

  1. Authenticates to the Progress Data Cloud using an API key.
  2. Retrieves the list of SharePoint subscriptions from the PDC API.
  3. For each site URL in the input file:
    1. Validates the site belongs to a configured subscription (skips with a warning if not).
    2. Connects to the site using PnP PowerShell.
    3. Grants RSC (Resource-Specific Consent) permissions to the Semaphore app.
    4. Enables custom scripting on the site.
    5. Queues a deployment via the PDC API.
  4. Outputs a summary of processed, skipped, and failed sites.

Prerequisites

Before running the script, ensure the following:

  • Windows PowerShell 5.1 or PowerShell 7+.
  • The PnP.PowerShell module is installed.
  • An Azure AD application (client) ID for PnP PowerShell authentication. To improve automation, use -ClientId and -CertificatePath.
  • A valid Progress Data Cloud API key for an appropriately permissioned user. For information about API key management, see the API key management script in the Use Progress Data Cloud guide.
  • A text file with one site URL per line. Blank lines are ignored. Host-only entries (for example, tenant.sharepoint.com) are automatically normalized to https://tenant.sharepoint.com.

Parameters

Parameter Required Description
InputFile Yes Path to a text file containing one site URL per line.
CloudUrl Yes The base URI of the Progress Data Cloud API (for example, https://tenant.data.progress.cloud).
ApiKey Yes The API key used to authenticate with Progress Data Cloud.
ClientId Yes The Azure AD application (client) ID for PnP PowerShell authentication.
SemaphoreAppId No App registration ID for RSC permissions. Defaults to 759a1e4d-98c0-4da1-80da-02e1b47cdd51.
SemaphoreDisplayName No Display name for the RSC grant. Defaults to Progress Data Cloud - Semaphore for SPO.

Usage examples

Deploy to all sites in a file:

.\Deploy-PdcToSpo.ps1 -InputFile .\sites.txt `
    -CloudUrl 'https://tenant.data.progress.cloud' `
    -ApiKey '***' `
    -ClientId '00000000-0000-0000-0000-000000000000'

Deploy with verbose logging and secure API key retrieval:

.\Deploy-PdcToSpo.ps1 -InputFile .\sites.txt `
    -CloudUrl 'https://tenant.data.progress.cloud' `
    -ApiKey (Get-Secret CloudApiKey) `
    -ClientId '00000000-0000-0000-0000-000000000000' `
    -Verbose

Preview actions without making changes (WhatIf):

.\Deploy-PdcToSpo.ps1 -InputFile .\sites.txt `
    -CloudUrl 'https://tenant.data.progress.cloud' `
    -ApiKey '***' `
    -ClientId '00000000-0000-0000-0000-000000000000' `
    -WhatIf

Important:

For enhanced security, consider storing your API key in a secure keystore rather than passing it as a plain text parameter. You can retrieve the key from your preferred secure storage solution (such as Azure Key Vault, HashiCorp Vault, or Windows Credential Manager).

Input file format

Create a text file with one SharePoint site URL per line. For example:

https://tenant.sharepoint.com/sites/Finance
https://tenant.sharepoint.com/sites/Marketing
https://tenant.sharepoint.com/sites/HR

PowerShell script

<#
.SYNOPSIS
Queues deployments of Progress Data Cloud to SharePoint sites listed in a file,
after validating each site is part of a configured subscription.

.DESCRIPTION
This script authenticates to the Progress Data Cloud using an API key, retrieves a list of SharePoint
subscriptions (tenants) from `/api/sharepointsubscription`, reads site URLs from the input file,
and for each site that belongs to a subscribed tenant:
  1) Connects with PnP.PowerShell using the provided ClientId. To improve automation, use 
     -ClientId and -CertificatePath. 
  2) Grants RSC (Resource-Specific Consent) permissions to the given app ID and display name.
  3) Enables custom scripting on the site (NoScriptSite = false).
  4) Queues a deployment by calling `/api/deployment/deploy?siteUrl={...}`.

Sites not belonging to any subscription authority are **skipped with a warning**. All REST calls
use retry logic with exponential backoff. The script supports -Verbose, -WhatIf, and -Confirm via
script-level cmdlet binding.

.PARAMETER InputFile
Path to a text file that contains one site URL (or host) per line.
Blank lines are ignored. If a line looks like a host (e.g., tenant.sharepoint.com),
the script normalizes it to `https://tenant.sharepoint.com`.

.PARAMETER CloudUrl
The base URI of the cloud API (e.g., https://tenant.data.progress.cloud).

.PARAMETER ApiKey
The API key used to connect to Progress Data Cloud for an appropriately permissioned user.

.PARAMETER ClientId
The Azure AD application (client) ID used to authenticate via PnP.PowerShell's `Connect-PnPOnline`.
This is typically a GUID (e.g., 00000000-0000-0000-0000-000000000000).

.PARAMETER SemaphoreAppId
(Optional) App registration (client) ID to which RSC permissions will be granted.
Defaults to `759a1e4d-98c0-4da1-80da-02e1b47cdd51`.

.PARAMETER SemaphoreDisplayName
(Optional) Display name of the app for the RSC grant. Defaults to
`Progress Data Cloud - Semaphore for SPO`.

.INPUTS
None. This script does not accept pipeline input.

.OUTPUTS
Information, Warning, and Verbose messages. A final summary line with counts:
Processed, Skipped, Failed.

.NOTES
- Requires Windows PowerShell 5.1 or PowerShell 7+.
- Requires the PnP.PowerShell module.
- Uses script-level [CmdletBinding()] to enable -Verbose, -WhatIf, -Confirm.
- Sets Strict Mode to Latest and `$ErrorActionPreference = 'Stop'`.
- REST calls include retry logic with exponential backoff.
- Sites not matching subscription authorities are skipped with warnings.

.EXAMPLE
PS> .\Deploy-PdcToSpo.ps1 -InputFile .\sites.txt -CloudUrl 'https://tenant.data.progress.cloud' -ApiKey '***' -ClientId '00000000-0000-0000-0000-000000000000'

Queues deployments for valid sites that belong to a configured subscription authority.
#>

#Requires -Version 5.1
#Requires -Modules PnP.PowerShell

[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [Parameter(Mandatory, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({ Test-Path $_ -PathType Leaf })]
    [string]$InputFile,

    [Parameter(Mandatory, Position = 1)]
    [ValidateNotNullOrEmpty()]
    [uri]$CloudUrl,

    [Parameter(Mandatory, Position = 2)]
    [ValidateNotNullOrEmpty()]
    [string]$ApiKey,

    [Parameter(Mandatory, Position = 3)]
    [ValidateNotNullOrEmpty()]
    [ValidatePattern('^[0-9a-fA-F\-]{36}
#39;)] [string]$ClientId, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SemaphoreAppId = '759a1e4d-98c0-4da1-80da-02e1b47cdd51', [Parameter()] [ValidateNotNullOrEmpty()] [string]$SemaphoreDisplayName = 'Progress Data Cloud - Semaphore for SPO' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Invoke-WithRetry { [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [int]$MaxAttempts = 3, [int]$InitialDelaySeconds = 2 ) $attempt = 0 $delay = $InitialDelaySeconds while ($attempt -lt $MaxAttempts) { try { return & $ScriptBlock } catch { $attempt++ $statusCode = $null try { $statusCode = $_.Exception.Response.StatusCode.Value__ } catch { } Write-Verbose ("Attempt {0} failed{1}: {2}" -f $attempt, ($(if ($statusCode) { " (HTTP $statusCode)" } else { "" })), $_.Exception.Message) if ($attempt -ge $MaxAttempts) { throw } Start-Sleep -Seconds $delay $delay = [math]::Min($delay * 2, 30) } } } function Get-CloudToken { [CmdletBinding()] param( [Parameter(Mandatory)] [uri]$BaseUrl, [Parameter(Mandatory)] [string]$ApiKey ) $uri = [uri]::new($BaseUrl, '/token') $body = @{ grant_type = 'apikey' key = $ApiKey } $requestParams = @{ Method = 'Post' ContentType = 'application/x-www-form-urlencoded' Uri = $uri Body = $body ErrorAction = 'Stop' } Write-Verbose "Requesting token from $($uri.AbsoluteUri)" $response = Invoke-WithRetry -ScriptBlock { Invoke-RestMethod @requestParams } if (-not $response.access_token) { throw "Token response missing 'access_token'." } return [string]$response.access_token } function New-AuthHeader { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Token ) return @{ Authorization = "Bearer $Token" Accept = 'application/json' } } function Get-SharePointSubscriptions { [CmdletBinding()] param( [Parameter(Mandatory)] [uri]$BaseUrl, [Parameter(Mandatory)] [hashtable]$Headers ) $uri = [uri]::new($BaseUrl, '/api/sharepointsubscription') $requestParams = @{ Method = 'Get' Uri = $uri Headers = $Headers ErrorAction = 'Stop' } Write-Verbose "Retrieving SharePoint subscriptions from $($uri.AbsoluteUri)" $response = Invoke-WithRetry -ScriptBlock { Invoke-RestMethod @requestParams } $items = @() if ($response -is [System.Collections.IEnumerable] -and -not ($response -is [string])) { $items = @($response) } else { $items = @($response) } $authorities = @() foreach ($item in $items) { $url = $item.url if (-not $url -and $item.PSObject.Properties['Url']) { $url = $item.Url } if ([string]::IsNullOrWhiteSpace($url)) { Write-Verbose "Skipping subscription entry without 'url'. Item: $($item | ConvertTo-Json -Depth 3)" continue } $authority = $url.Trim().ToLowerInvariant() if ($authority -notmatch '^[a-z0-9.-]+
#39;) { Write-Verbose "Subscription 'url' may not be a pure host: '$url' (normalized: '$authority')" } $authorities += $authority } $authorities = $authorities | Sort-Object -Unique return [pscustomobject]@{ Raw = $response Authorities = $authorities } } function Test-SiteInSubscriptions { [CmdletBinding()] param( [Parameter(Mandatory)] [uri]$SiteUri, [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]]$SubscriptionAuthorities ) $auths = $SubscriptionAuthorities if ($null -eq $auths) { $auths = @() } $siteAuthority = $SiteUri.Authority.ToLowerInvariant() return ($auths -contains $siteAuthority) } function Queue-Deployment { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [uri]$BaseUrl, [Parameter(Mandatory)] [hashtable]$Headers, [Parameter(Mandatory)] [uri]$SiteUrl ) $encodedSite = [System.Net.WebUtility]::UrlEncode($SiteUrl.AbsoluteUri) $relative = "/api/deployment/deploy?siteUrl=$encodedSite" $uri = [uri]::new($BaseUrl, $relative) $requestParams = @{ Method = 'Post' Uri = $uri Headers = $Headers ErrorAction = 'Stop' } if ($PSCmdlet.ShouldProcess($SiteUrl.AbsoluteUri, "Queue deployment")) { Write-Verbose "Queueing deployment for $($SiteUrl.AbsoluteUri)" Invoke-WithRetry -ScriptBlock { Invoke-RestMethod @requestParams } | Out-Null } } function Ensure-CustomScriptingEnabled { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [uri]$SiteUrl ) if ($PSCmdlet.ShouldProcess($SiteUrl.AbsoluteUri, "Enable custom scripting")) { Write-Verbose "Setting NoScriptSite = false for $($SiteUrl.AbsoluteUri)" Set-PnPSite -Identity $SiteUrl.AbsoluteUri -NoScriptSite $false -ErrorAction Stop } } function Grant-RSCPermissions { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$AppId, [Parameter(Mandatory)] [string]$DisplayName ) if ($PSCmdlet.ShouldProcess($DisplayName, "Grant RSC permissions (FullControl)")) { Write-Verbose "Granting RSC permissions to AppId=$AppId DisplayName='$DisplayName'" Grant-PnPAzureADAppSitePermission -AppId $AppId -DisplayName $DisplayName -Permissions FullControl -ErrorAction Stop } } # --- Input: load and validate site URLs --- $rawSites = Get-Content -Path $InputFile if (-not $rawSites -or $rawSites.Count -eq 0) { throw "No site URLs found in '$InputFile'." } $sites = @() foreach ($line in $rawSites) { $trimmed = $line.Trim() if ([string]::IsNullOrWhiteSpace($trimmed)) { continue } try { $sites += [uri]$trimmed } catch { if ($trimmed -match '^[a-z0-9.-]+(\.[a-z0-9.-]+)+(/.*)?
#39;) { $candidate = "https://$trimmed" try { $sites += [uri]$candidate Write-Verbose "Normalized site URL '$trimmed' to '$candidate'." } catch { Write-Warning "Skipping invalid URL in input: '$trimmed'" } } else { Write-Warning "Skipping invalid URL in input: '$trimmed'" } } } if ($sites.Count -eq 0) { throw "No valid site URLs to process after validation." } # --- Auth & subscriptions --- try { $token = Get-CloudToken -BaseUrl $CloudUrl -ApiKey $ApiKey $header = New-AuthHeader -Token $token $subscriptions = Get-SharePointSubscriptions -BaseUrl $CloudUrl -Headers $header } catch { Write-Error "Initialization failed: $($_.Exception.Message)" exit 1 } if (-not $subscriptions.Authorities -or $subscriptions.Raw.Count -eq 0) { Write-Warning "No SharePoint subscriptions were returned by the service. All sites will be skipped." } # --- Process each site --- $processed = 0 $failed = 0 $skipped = 0 foreach ($site in $sites) { try { if (Test-SiteInSubscriptions -SiteUri $site -SubscriptionAuthorities $subscriptions.Authorities) { Write-Verbose "Connecting to $($site.AbsoluteUri)" if ($PSCmdlet.ShouldProcess($site.AbsoluteUri, "Connect PnP")) { Connect-PnPOnline -Url $site.AbsoluteUri -ClientId $ClientId -ErrorAction Stop } try { Grant-RSCPermissions -AppId $SemaphoreAppId -DisplayName $SemaphoreDisplayName Ensure-CustomScriptingEnabled -SiteUrl $site Queue-Deployment -BaseUrl $CloudUrl -Headers $header -SiteUrl $site $processed++ Write-Information "Queued deployment for $($site.AbsoluteUri)" } finally { Write-Verbose "Disconnecting from $($site.AbsoluteUri)" Disconnect-PnPOnline -ErrorAction SilentlyContinue } } else { $skipped++ Write-Warning "Site $($site.AbsoluteUri) is not part of a configured subscription; skipping deployment." } } catch { $failed++ Write-Warning "Failed for site $($site.AbsoluteUri): $($_.Exception.Message)" Disconnect-PnPOnline -ErrorAction SilentlyContinue } } Write-Information "Finished. Processed=$processed; Skipped=$skipped; Failed=$failed"
TitleResults for “How to create a CRG?”Also Available inAlert