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 |
2 changes: 1 addition & 1 deletion .github/actions/update-index/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ runs:
ClientID: ${{ inputs.ClientID }}
PrivateKey: ${{ inputs.PrivateKey }}
Script: |
${{ github.action_path }}/src/main.ps1
${{ github.action_path }}/src/Invoke-UpdateIndex.ps1
42 changes: 42 additions & 0 deletions .github/actions/update-index/src/Invoke-UpdateIndex.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#Requires -Version 7.0

<#
.SYNOPSIS
Updates generated index content for the docs repository.

.DESCRIPTION
Orchestrates the update-index action flow by collecting organization repository
metadata and regenerating module catalog documentation artifacts.

.EXAMPLE
./Invoke-UpdateIndex.ps1
#>
[CmdletBinding()]
param()

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

Import-Module -Name (Join-Path $PSScriptRoot 'update-index.Helpers.psm1')

LogGroup 'Initialize update-index run' {
Write-Host "Starting update-index in [$PSScriptRoot]"
}

LogGroup 'Collect repositories' {
$repos = Show-RepoList
Write-Host "Repository collection complete: $($repos.Count) records"
}

LogGroup 'Skipped generators' {
Write-Host 'Update-ActionList is currently disabled in Invoke-UpdateIndex.ps1'
Write-Host 'Update-FunctionAppList is currently disabled in Invoke-UpdateIndex.ps1'
}

LogGroup 'Update module catalog docs' {
Update-ModuleList -Repos $repos
}

LogGroup 'Finalize update-index run' {
Write-Host 'update-index run completed'
}
23 changes: 0 additions & 23 deletions .github/actions/update-index/src/main.ps1

This file was deleted.

96 changes: 96 additions & 0 deletions .github/scripts/docs/Cleanup-PreviewDocs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#Requires -Version 7.0

<#
.SYNOPSIS
Removes pull request preview artifacts and related environment state.

.DESCRIPTION
Deletes previews/pr-<number> from gh-pages, deactivates and removes preview
deployments, deletes the preview environment, and updates the PR comment.

.EXAMPLE
./Cleanup-PreviewDocs.ps1 `
-Repository "PSModule/docs" `
-Token $token `
-PullRequestNumber 42 `
-PreviewUrl "https://psmodule.io/docs/previews/pr-42/" `
-EnvironmentName "pr-preview-42"
#>
[CmdletBinding()]
param(
# Repository in owner/name format.
[Parameter(Mandatory = $true)]
[string]$Repository,
# GitHub App token used for authenticated git and gh operations.
[Parameter(Mandatory = $true)]
[string]$Token,
# Pull request number used for preview path and comment targeting.
[Parameter(Mandatory = $true)]
[int]$PullRequestNumber,
# Public preview URL for PR comments.
[Parameter(Mandatory = $true)]
[string]$PreviewUrl,
# Environment name to clean up from repository environments.
[Parameter(Mandatory = $true)]
[string]$EnvironmentName,
# Working directory used to clone gh-pages.
[string]$PagesDirectory = '_pages',
# Branch used as the Pages source branch.
[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
}

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

$commentBody = "<!-- docs-pr-preview -->`n🧹 Preview removed: $PreviewUrl"
Write-IssueComment -Repository $Repository -IssueNumber $PullRequestNumber -Marker '<!-- docs-pr-preview -->' -Body $commentBody
84 changes: 84 additions & 0 deletions .github/scripts/docs/Publish-LiveDocs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#Requires -Version 7.0

<#
.SYNOPSIS
Publishes live docs content to the gh-pages branch.

.DESCRIPTION
Clones gh-pages, replaces live content while keeping previews, and pushes
the updated branch using the GitHub App token identity.

.EXAMPLE
./Publish-LiveDocs.ps1 `
-Repository "PSModule/docs" `
-Token $token `
-BuildDirectory "$PWD/src/site" `
-CommitSha $env:GITHUB_SHA
#>
[CmdletBinding()]
param(
# Repository in owner/name format.
[Parameter(Mandatory = $true)]
[string]$Repository,
# GitHub App token used for authenticated git and gh operations.
[Parameter(Mandatory = $true)]
[string]$Token,
# Full path to the built static site directory.
[Parameter(Mandatory = $true)]
[string]$BuildDirectory,
# Commit SHA used in the publish commit message.
[Parameter(Mandatory = $true)]
[string]$CommitSha,
# Working directory used to clone gh-pages.
[string]$PagesDirectory = '_pages',
# Branch used as the Pages source branch.
[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)) {
Write-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")

Write-WorkflowOutput -Name 'has_changes' -Value 'true'
87 changes: 87 additions & 0 deletions .github/scripts/docs/Publish-PreviewDocs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#Requires -Version 7.0

<#
.SYNOPSIS
Publishes pull request preview docs content to gh-pages.

.DESCRIPTION
Writes the PR preview build output into previews/pr-<number> on gh-pages,
pushes branch updates, and ensures the preview comment is present on the PR.

.EXAMPLE
./Publish-PreviewDocs.ps1 `
-Repository "PSModule/docs" `
-Token $token `
-PullRequestNumber 42 `
-PreviewUrl "https://psmodule.io/docs/previews/pr-42/" `
-BuildDirectory "$PWD/src/site"
#>
[CmdletBinding()]
param(
# Repository in owner/name format.
[Parameter(Mandatory = $true)]
[string]$Repository,
# GitHub App token used for authenticated git and gh operations.
[Parameter(Mandatory = $true)]
[string]$Token,
# Pull request number used for preview path and comment targeting.
[Parameter(Mandatory = $true)]
[int]$PullRequestNumber,
# Public preview URL for PR comments and environment metadata.
[Parameter(Mandatory = $true)]
[string]$PreviewUrl,
# Full path to the built static site directory.
[Parameter(Mandatory = $true)]
[string]$BuildDirectory,
# Working directory used to clone gh-pages.
[string]$PagesDirectory = '_pages',
# Branch used as the Pages source branch.
[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
Write-IssueComment -Repository $Repository -IssueNumber $PullRequestNumber -Marker '<!-- docs-pr-preview -->' -Body $commentBody

Write-WorkflowOutput -Name 'url' -Value $PreviewUrl
Loading
Loading