Skip to content
Open
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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
pull_request:
push:
branches:
- master

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install test dependencies
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
Install-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Force

- name: Analyze controller
shell: pwsh
run: Invoke-ScriptAnalyzer dependency-updates -Recurse -Severity Warning,Error -EnableExit

- name: Test controller
shell: pwsh
run: Invoke-Pester dependency-updates/Tests -CI
122 changes: 122 additions & 0 deletions .github/workflows/update-powershell-dependencies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: PowerShell dependency updates

on:
schedule:
- cron: '17 4 * * 1'
workflow_dispatch:
inputs:
repository:
description: Repository name from dependency-updates/targets.json, or all
required: true
default: all
type: string
allow_major_version_upgrade:
description: Allow dependencies to cross major-version boundaries
required: true
default: false
type: boolean

permissions:
contents: read

concurrency:
group: powershell-dependency-updates
cancel-in-progress: false

jobs:
plan:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.targets.outputs.targets }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- id: targets
name: Select repositories
shell: pwsh
env:
REQUESTED_REPOSITORY: ${{ inputs.repository }}
run: |
$targets = @(Get-Content dependency-updates/targets.json -Raw | ConvertFrom-Json)
$requestedRepository = $env:REQUESTED_REPOSITORY
if ([string]::IsNullOrWhiteSpace($requestedRepository)) {
$requestedRepository = 'all'
}

if ($requestedRepository -ne 'all') {
$targets = @($targets | Where-Object repository -EQ $requestedRepository)
}

if ($targets.Count -eq 0) {
throw "Repository '$requestedRepository' is not configured in dependency-updates/targets.json."
}

$matrix = ConvertTo-Json -InputObject @($targets) -Compress
"targets=$matrix" >> $env:GITHUB_OUTPUT

update:
needs: plan
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
target: ${{ fromJSON(needs.plan.outputs.targets) }}
steps:
- name: Create repository token
id: app-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
with:
app-id: ${{ secrets.DEPENDENCY_UPDATE_APP_ID }}
private-key: ${{ secrets.DEPENDENCY_UPDATE_APP_PRIVATE_KEY }}
owner: AtlassianPS
repositories: ${{ matrix.target.repository }}
permission-contents: write
permission-issues: write
permission-pull-requests: write
permission-workflows: write

- name: Check out controller
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: controller

- name: Check out target repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: AtlassianPS/${{ matrix.target.repository }}
token: ${{ steps.app-token.outputs.token }}
path: target
fetch-depth: 0
persist-credentials: false

- name: Update dependencies
shell: pwsh
env:
ALLOW_MAJOR_VERSION_UPGRADE: ${{ inputs.allow_major_version_upgrade }}
GITHUB_TOKEN: ${{ github.token }}
run: |
$parameters = @{
RepositoryPath = './target'
ModuleName = '${{ matrix.target.module }}'
}
if ($env:ALLOW_MAJOR_VERSION_UPGRADE -eq 'true') {
$parameters.AllowMajorVersionUpgrade = $true
}

./controller/dependency-updates/Update-RepositoryDependencies.ps1 @parameters

- name: Create or refresh pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.app-token.outputs.token }}
path: target
branch: automation/update-powershell-dependencies
delete-branch: true
commit-message: 'chore(deps): update PowerShell dependencies'
title: 'chore(deps): update PowerShell dependencies'
body: |
Updates PowerShell dependencies and synchronized AtlassianPS.Standards references.

Generated by the centralized dependency workflow in `AtlassianPS/.github`.
labels: ${{ matrix.target.labels }}
32 changes: 32 additions & 0 deletions dependency-updates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Central dependency updates

The organization `.github` repository owns dependency-update orchestration.
Target repositories contain dependency declarations and consistency tests, but no updater script.

The scheduled workflow reads `targets.json`, checks out each repository, and invokes the update
engine published by `AtlassianPS.Standards`.
It creates or refreshes `automation/update-powershell-dependencies` when files change.
The controller and workflow contract are validated by the repository's `CI` workflow.

## GitHub App

Configure an organization GitHub App with access only to the repositories in `targets.json`.
It needs these repository permissions:

- Contents: read and write
- Issues: read and write
- Pull requests: read and write
- Workflows: read and write

Expose its credentials to this repository as organization secrets:

- `DEPENDENCY_UPDATE_APP_ID`
- `DEPENDENCY_UPDATE_APP_PRIVATE_KEY`

The workflow requests a short-lived token for one target repository at a time.

## Manual runs

Run **PowerShell dependency updates** from the Actions page.
Use `all` to process every configured repository or provide one repository name from `targets.json`.
Scheduled runs preserve dependency major versions; manual runs can explicitly allow major upgrades.
121 changes: 121 additions & 0 deletions dependency-updates/Tests/Update-RepositoryDependencies.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.7'; MaximumVersion = '5.999' }

BeforeAll {
$scriptPath = Join-Path $PSScriptRoot '../Update-RepositoryDependencies.ps1'
. $scriptPath -RepositoryPath $TestDrive -ModuleName TestModule

function New-TestRepository {
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '',
Justification = 'This helper only creates isolated Pester TestDrive fixtures.'
)]
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[String]$Path
)

$null = New-Item -ItemType Directory -Path (Join-Path $Path 'Tools') -Force
$null = New-Item -ItemType Directory -Path (Join-Path $Path 'TestModule') -Force
$null = New-Item -ItemType Directory -Path (Join-Path $Path '.github/workflows') -Force

@"
@(
@{ ModuleName = 'AtlassianPS.Standards'; RequiredVersion = '0.3.1'; MaximumVersion = '0.3.1' }
)
"@ | Set-Content -LiteralPath (Join-Path $Path 'Tools/build.requirements.psd1')
"@{ RequiredModules = @() }" |
Set-Content -LiteralPath (Join-Path $Path 'TestModule/TestModule.psd1')
}
}

Describe 'Update-RepositoryDependencies' {
It 'fails before mutation when required dependency files are missing' {
$repositoryPath = Join-Path $TestDrive 'missing-files'
$null = New-Item -ItemType Directory -Path $repositoryPath

{ Invoke-RepositoryDependencyUpdate -RepositoryPath $repositoryPath -ModuleName TestModule -StandardsVersion '0.3.1' } |
Should -Throw '*Required dependency file was not found*'
}

It 'does not initialize dependency tooling under WhatIf' {
$repositoryPath = Join-Path $TestDrive 'what-if'
New-TestRepository -Path $repositoryPath
Mock Get-PSRepository { throw 'Should not be called' }

Invoke-RepositoryDependencyUpdate `
-RepositoryPath $repositoryPath `
-ModuleName TestModule `
-StandardsVersion '0.3.1' `
-WhatIf

Should -Invoke Get-PSRepository -Times 0
}

It 'uses the default Standards version from the script entry point' {
$repositoryPath = Join-Path $TestDrive 'entry-point'
New-TestRepository -Path $repositoryPath

{
& $scriptPath -RepositoryPath $repositoryPath -ModuleName TestModule -WhatIf
} | Should -Not -Throw
}

It 'passes the declared files and major-upgrade choice to Standards' {
$repositoryPath = Join-Path $TestDrive 'delegation'
New-TestRepository -Path $repositoryPath
Set-Content `
-LiteralPath (Join-Path $repositoryPath '.github/workflows/ci.yml') `
-Value 'uses: AtlassianPS/AtlassianPS.Standards/.github/workflows/module_ci.yml@0000000000000000000000000000000000000000 # v0.3.0'

Mock Get-PSRepository { [PSCustomObject]@{ InstallationPolicy = 'Trusted' } }
Mock Get-Module { [PSCustomObject]@{ Version = [Version]'0.3.1' } }
Mock Import-Module
Mock Invoke-StandardsDependencyUpdate { [PSCustomObject]@{ Changed = $true } }
Mock Invoke-RestMethod { [PSCustomObject]@{ sha = 'a' * 40 } }

$result = Invoke-RepositoryDependencyUpdate `
-RepositoryPath $repositoryPath `
-ModuleName TestModule `
-StandardsVersion '0.3.1' `
-AllowMajorVersionUpgrade

$result.Changed | Should -BeTrue
Should -Invoke Invoke-StandardsDependencyUpdate -Times 1 -ParameterFilter {
$Parameters.BuildRequirementsPath -eq (Join-Path $repositoryPath 'Tools/build.requirements.psd1') -and
$Parameters.ManifestPath -eq (Join-Path $repositoryPath 'TestModule/TestModule.psd1') -and
$Parameters.AllowMajorVersionUpgrade
}
}

It 'updates yml and yaml references while preserving encoding and newlines' {
$repositoryPath = Join-Path $TestDrive 'workflow-files'
New-TestRepository -Path $repositoryPath
$workflowRoot = Join-Path $repositoryPath '.github/workflows'
$oldReference = 'uses: AtlassianPS/AtlassianPS.Standards/.github/workflows/module_ci.yml@0000000000000000000000000000000000000000 # v0.3.0'
$newSha = 'b' * 40

$bomPath = Join-Path $workflowRoot 'ci.yml'
$plainPath = Join-Path $workflowRoot 'release.yaml'
[System.IO.File]::WriteAllText($bomPath, "$oldReference`r`n", [System.Text.UTF8Encoding]::new($true))
[System.IO.File]::WriteAllText($plainPath, "$oldReference`n", [System.Text.UTF8Encoding]::new($false))
Mock Invoke-RestMethod { [PSCustomObject]@{ sha = $newSha } }

Sync-StandardsWorkflowReference -ProjectRoot $repositoryPath -Version '0.3.1'

$bomBytes = [System.IO.File]::ReadAllBytes($bomPath)
$bomBytes[0..2] | Should -Be @(0xEF, 0xBB, 0xBF)
[System.Text.Encoding]::UTF8.GetString($bomBytes) | Should -Match "# v0\.3\.1`r`n$"
(Get-Content -LiteralPath $plainPath -Raw) | Should -Match "# v0\.3\.1`n$"
[System.IO.File]::ReadAllBytes($plainPath)[0] | Should -Not -Be 0xEF
}

It 'fails when GitHub does not return a commit SHA' {
$repositoryPath = Join-Path $TestDrive 'invalid-sha'
New-TestRepository -Path $repositoryPath
Mock Invoke-RestMethod { [PSCustomObject]@{ sha = 'invalid' } }

{ Sync-StandardsWorkflowReference -ProjectRoot $repositoryPath -Version '0.3.1' } |
Should -Throw '*valid commit*'
}
}
Loading