Skip to content
Draft
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
51 changes: 51 additions & 0 deletions .github/PR-PREVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# PR docs preview setup

This repository publishes docs previews for pull requests under:

- `https://psmodule.io/docs/previews/pr-<number>/`

## What the workflow does

1. On PR open/reopen/synchronize:
- builds docs with preview-specific `site_url`,
- updates `previews/pr-<number>/` content by pushing directly to `gh-pages` as the Scribbler app,
- comments on the source PR with the preview URL,
- reports the preview URL through a named environment (`pr-preview-<number>`).
2. On PR close (merge or abandon):
- removes `previews/pr-<number>/` by pushing directly to `gh-pages` as the Scribbler app,
- deletes all preview deployments and the preview environment.

## Flow diagram

```mermaid
flowchart TD
A[Contributor opens or updates PR to main] --> B[Docs workflow builds preview site]
B --> C[Scribbler app token pushes preview files to gh-pages under previews/pr-N]
C --> D[Workflow sets environment pr-preview-N with preview URL]
D --> E[Workflow comments on PR with preview link]
E --> F[Contributor iterates on PR]
F --> B
G[PR closed: merged or abandoned] --> H[Cleanup job runs]
H --> I[Scribbler app token removes previews/pr-N from gh-pages]
I --> J[Workflow deletes preview deployments + environment]
J --> K[Workflow updates PR comment: preview removed]
```

## Required repository configuration

1. Ensure `gh-pages` branch exists.
2. Configure GitHub Pages to publish from `gh-pages`.
3. Protect `gh-pages` and restrict push access so **only Scribbler bot app** can push.
4. In the `gh-pages` branch protection/ruleset, add **Scribbler bot app** as the only actor allowed to bypass required pull requests and any required status checks for that branch.

## Scribbler GitHub App permissions

The app needs the following repository permissions:

| Permission | Access | Why |
| --- | --- | --- |
| Metadata | Read | Required baseline for API access |
| Contents | Read & write | Push docs and preview content directly to `gh-pages` |
| Issues | Read & write | Post and update preview comments on PR threads |
| Deployments | Read & write | Deactivate and delete preview deployments |
| Administration | Read & write | Delete per-PR environments during cleanup |
70 changes: 70 additions & 0 deletions .github/scripts/docs/Cleanup-PreviewDocs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
param(
[Parameter(Mandatory = $true)]
[string]$Repository,
[Parameter(Mandatory = $true)]
[string]$Token,
[Parameter(Mandatory = $true)]
[int]$PullRequestNumber,
[Parameter(Mandatory = $true)]
[string]$PreviewUrl,
[Parameter(Mandatory = $true)]
[string]$EnvironmentName,
[string]$PagesDirectory = '_pages',
[string]$BaseBranch = 'gh-pages'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

. "$PSScriptRoot/Shared.ps1"

$clonedPages = $true
Invoke-Git -Arguments @(
'clone',
'--no-tags',
'--depth', '1',
'--branch', $BaseBranch,
"https://x-access-token:$Token@github.com/$Repository.git",
$PagesDirectory
) -AllowFailure | Out-Null

if (-not (Test-Path -LiteralPath $PagesDirectory -PathType Container)) {
$clonedPages = $false
}

if ($clonedPages) {
$previewDirectory = Join-Path $PagesDirectory "previews/pr-$PullRequestNumber"
if (Test-Path -LiteralPath $previewDirectory) {
Remove-Item -LiteralPath $previewDirectory -Recurse -Force
}

Invoke-Git -Arguments @('-C', $PagesDirectory, 'config', 'user.name', 'scribbler-bot[bot]')
Invoke-Git -Arguments @('-C', $PagesDirectory, 'config', 'user.email', 'scribe@psmodule.io')
Invoke-Git -Arguments @('-C', $PagesDirectory, 'add', '-A')

$status = (& git -C $PagesDirectory status --porcelain)
if (-not [string]::IsNullOrWhiteSpace($status)) {
Invoke-Git -Arguments @('-C', $PagesDirectory, 'commit', '-m', "Remove docs preview for PR #$PullRequestNumber")
Invoke-Git -Arguments @('-C', $PagesDirectory, 'push', 'origin', "HEAD:refs/heads/$BaseBranch")
}
}

$env:GH_TOKEN = $Token
$deploymentsJson = Invoke-Gh -Arguments @('api', "repos/$Repository/deployments?environment=$EnvironmentName&per_page=100")
$deployments = @($deploymentsJson | ConvertFrom-Json)

foreach ($deployment in $deployments) {
Invoke-Gh -Arguments @('api', '--method', 'POST', "repos/$Repository/deployments/$($deployment.id)/statuses", '-f', 'state=inactive') | Out-Null
Invoke-Gh -Arguments @('api', '--method', 'DELETE', "repos/$Repository/deployments/$($deployment.id)") | Out-Null
}

$deleteEnvironmentExit = Invoke-Gh -Arguments @('api', '--method', 'DELETE', "repos/$Repository/environments/$EnvironmentName") -AllowFailure
if ($LASTEXITCODE -ne 0) {
$environmentCheckExit = Invoke-Gh -Arguments @('api', "repos/$Repository/environments/$EnvironmentName") -AllowFailure
if ($LASTEXITCODE -eq 0) {
throw "Failed to delete environment '$EnvironmentName'."
}
}

$commentBody = "<!-- docs-pr-preview -->`n🧹 Preview removed: $PreviewUrl"
Upsert-IssueComment -Repository $Repository -IssueNumber $PullRequestNumber -Marker '<!-- docs-pr-preview -->' -Body $commentBody
60 changes: 60 additions & 0 deletions .github/scripts/docs/Publish-LiveDocs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
param(
[Parameter(Mandatory = $true)]
[string]$Repository,
[Parameter(Mandatory = $true)]
[string]$Token,
[Parameter(Mandatory = $true)]
[string]$BuildDirectory,
[Parameter(Mandatory = $true)]
[string]$CommitSha,
[string]$PagesDirectory = '_pages',
[string]$BaseBranch = 'gh-pages'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

. "$PSScriptRoot/Shared.ps1"

if (-not (Test-Path -LiteralPath $BuildDirectory -PathType Container)) {
throw "Build directory '$BuildDirectory' does not exist."
}

Invoke-Git -Arguments @(
'clone',
'--no-tags',
'--depth', '1',
'--branch', $BaseBranch,
"https://x-access-token:$Token@github.com/$Repository.git",
$PagesDirectory
) -AllowFailure | Out-Null

if (-not (Test-Path -LiteralPath $PagesDirectory -PathType Container)) {
throw "$BaseBranch branch is required for branch-based deployment."
}

Get-ChildItem -LiteralPath $PagesDirectory -Force |
Where-Object { $_.Name -notin @('.git', 'previews') } |
Remove-Item -Recurse -Force

Get-ChildItem -LiteralPath $BuildDirectory -Force |
ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $PagesDirectory -Recurse -Force
}

New-Item -Path (Join-Path $PagesDirectory '.nojekyll') -ItemType File -Force | Out-Null

Invoke-Git -Arguments @('-C', $PagesDirectory, 'config', 'user.name', 'scribbler-bot[bot]')
Invoke-Git -Arguments @('-C', $PagesDirectory, 'config', 'user.email', 'scribe@psmodule.io')
Invoke-Git -Arguments @('-C', $PagesDirectory, 'add', '-A')

$status = (& git -C $PagesDirectory status --porcelain)
if ([string]::IsNullOrWhiteSpace($status)) {
Set-WorkflowOutput -Name 'has_changes' -Value 'false'
exit 0
}

Invoke-Git -Arguments @('-C', $PagesDirectory, 'commit', '-m', "Deploy docs from $CommitSha")
Invoke-Git -Arguments @('-C', $PagesDirectory, 'push', 'origin', "HEAD:refs/heads/$BaseBranch")

Set-WorkflowOutput -Name 'has_changes' -Value 'true'
61 changes: 61 additions & 0 deletions .github/scripts/docs/Publish-PreviewDocs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
param(
[Parameter(Mandatory = $true)]
[string]$Repository,
[Parameter(Mandatory = $true)]
[string]$Token,
[Parameter(Mandatory = $true)]
[int]$PullRequestNumber,
[Parameter(Mandatory = $true)]
[string]$PreviewUrl,
[Parameter(Mandatory = $true)]
[string]$BuildDirectory,
[string]$PagesDirectory = '_pages',
[string]$BaseBranch = 'gh-pages'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

. "$PSScriptRoot/Shared.ps1"

if (-not (Test-Path -LiteralPath $BuildDirectory -PathType Container)) {
throw "Build directory '$BuildDirectory' does not exist."
}

Invoke-Git -Arguments @(
'clone',
'--no-tags',
'--depth', '1',
'--branch', $BaseBranch,
"https://x-access-token:$Token@github.com/$Repository.git",
$PagesDirectory
)

$previewDirectory = Join-Path $PagesDirectory "previews/pr-$PullRequestNumber"
if (Test-Path -LiteralPath $previewDirectory) {
Remove-Item -LiteralPath $previewDirectory -Recurse -Force
}

New-Item -Path $previewDirectory -ItemType Directory -Force | Out-Null
Get-ChildItem -LiteralPath $BuildDirectory -Force |
ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $previewDirectory -Recurse -Force
}

New-Item -Path (Join-Path $PagesDirectory '.nojekyll') -ItemType File -Force | Out-Null

Invoke-Git -Arguments @('-C', $PagesDirectory, 'config', 'user.name', 'scribbler-bot[bot]')
Invoke-Git -Arguments @('-C', $PagesDirectory, 'config', 'user.email', 'scribe@psmodule.io')
Invoke-Git -Arguments @('-C', $PagesDirectory, 'add', '-A')

$status = (& git -C $PagesDirectory status --porcelain)
if (-not [string]::IsNullOrWhiteSpace($status)) {
Invoke-Git -Arguments @('-C', $PagesDirectory, 'commit', '-m', "Update docs preview for PR #$PullRequestNumber")
Invoke-Git -Arguments @('-C', $PagesDirectory, 'push', 'origin', "HEAD:refs/heads/$BaseBranch")
}

$commentBody = "<!-- docs-pr-preview -->`n✅ Preview is ready: $PreviewUrl"
$env:GH_TOKEN = $Token
Upsert-IssueComment -Repository $Repository -IssueNumber $PullRequestNumber -Marker '<!-- docs-pr-preview -->' -Body $commentBody

Set-WorkflowOutput -Name 'url' -Value $PreviewUrl
13 changes: 13 additions & 0 deletions .github/scripts/docs/Set-ZensicalSiteUrl.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
param(
[Parameter(Mandatory = $true)]
[string]$ConfigPath,
[Parameter(Mandatory = $true)]
[string]$SiteUrl
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$content = Get-Content -LiteralPath $ConfigPath -Raw
$updated = $content -replace '(?m)^site_url = ".*"$', "site_url = `"$SiteUrl`""
Set-Content -LiteralPath $ConfigPath -Value $updated
73 changes: 73 additions & 0 deletions .github/scripts/docs/Shared.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Invoke-Git {
param(
[Parameter(Mandatory = $true)]
[string[]]$Arguments,
[switch]$AllowFailure
)

& git @Arguments
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0 -and -not $AllowFailure) {
throw "git $($Arguments -join ' ') failed with exit code $exitCode."
}

return $exitCode
}

function Set-WorkflowOutput {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Value
)

if (-not $env:GITHUB_OUTPUT) {
throw 'GITHUB_OUTPUT is not defined.'
}

"$Name=$Value" >> $env:GITHUB_OUTPUT
}

function Invoke-Gh {
param(
[Parameter(Mandatory = $true)]
[string[]]$Arguments,
[switch]$AllowFailure
)

$output = & gh @Arguments
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0 -and -not $AllowFailure) {
throw "gh $($Arguments -join ' ') failed with exit code $exitCode."
}

return $output
}

function Upsert-IssueComment {
param(
[Parameter(Mandatory = $true)]
[string]$Repository,
[Parameter(Mandatory = $true)]
[int]$IssueNumber,
[Parameter(Mandatory = $true)]
[string]$Marker,
[Parameter(Mandatory = $true)]
[string]$Body
)

$commentsJson = Invoke-Gh -Arguments @('api', "repos/$Repository/issues/$IssueNumber/comments?per_page=100")
$comments = @($commentsJson | ConvertFrom-Json)
$existing = $comments | Where-Object { $_.body -like "*$Marker*" } | Select-Object -First 1

if ($null -ne $existing) {
Invoke-Gh -Arguments @('api', '--method', 'PATCH', "repos/$Repository/issues/comments/$($existing.id)", '-f', "body=$Body") | Out-Null
return
}

Invoke-Gh -Arguments @('api', '--method', 'POST', "repos/$Repository/issues/$IssueNumber/comments", '-f', "body=$Body") | Out-Null
}
Loading