Skip to content
Merged
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
159 changes: 159 additions & 0 deletions .github/actions/github-housekeeping/Invoke-PowerForgeHousekeeping.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'

$repoRoot = (Resolve-Path (Join-Path $env:GITHUB_ACTION_PATH "../..")).Path
$project = Join-Path $repoRoot "PowerForge.Cli/PowerForge.Cli.csproj"

function Format-GiB {
param([long] $Bytes)

if ($Bytes -le 0) {
return '0.0 GiB'
}

return ('{0:N1} GiB' -f ($Bytes / 1GB))
}

function Write-MarkdownSummary {
param([string[]] $Lines)

if ([string]::IsNullOrWhiteSpace($env:GITHUB_STEP_SUMMARY)) {
return
}

Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ($Lines -join [Environment]::NewLine)
}

function Resolve-ConfigPath {
$configPath = $env:INPUT_CONFIG_PATH
if ([string]::IsNullOrWhiteSpace($configPath)) {
$configPath = '.powerforge/github-housekeeping.json'
}

if ([System.IO.Path]::IsPathRooted($configPath)) {
return [System.IO.Path]::GetFullPath($configPath)
}

if ([string]::IsNullOrWhiteSpace($env:GITHUB_WORKSPACE)) {
throw 'GITHUB_WORKSPACE is not set.'
}

return [System.IO.Path]::GetFullPath((Join-Path $env:GITHUB_WORKSPACE $configPath))
}

function Write-HousekeepingSummary {
param([pscustomobject] $Envelope)

if (-not $Envelope.result) {
return
}

$result = $Envelope.result
$lines = @(
"### GitHub housekeeping",
"",
"- Mode: $(if ($result.dryRun) { 'dry-run' } else { 'apply' })",
"- Requested sections: $((@($result.requestedSections) -join ', '))",
"- Completed sections: $((@($result.completedSections) -join ', '))",
"- Failed sections: $((@($result.failedSections) -join ', '))",
"- Success: $(if ($Envelope.success) { 'yes' } else { 'no' })"
)

if ($result.message) {
$lines += "- Message: $($result.message)"
}

if ($result.caches) {
$lines += ''
$lines += '#### Caches'
if ($result.caches.usageBefore) {
$lines += "- Usage before: $($result.caches.usageBefore.activeCachesCount) caches, $(Format-GiB ([long]$result.caches.usageBefore.activeCachesSizeInBytes))"
}
if ($result.caches.usageAfter) {
$lines += "- Usage after: $($result.caches.usageAfter.activeCachesCount) caches, $(Format-GiB ([long]$result.caches.usageAfter.activeCachesSizeInBytes))"
}
$lines += "- Planned deletes: $($result.caches.plannedDeletes) ($(Format-GiB ([long]$result.caches.plannedDeleteBytes)))"
$lines += "- Deleted: $($result.caches.deletedCaches) ($(Format-GiB ([long]$result.caches.deletedBytes)))"
$lines += "- Failed deletes: $($result.caches.failedDeletes)"
}

if ($result.artifacts) {
$lines += ''
$lines += '#### Artifacts'
$lines += "- Planned deletes: $($result.artifacts.plannedDeletes) ($(Format-GiB ([long]$result.artifacts.plannedDeleteBytes)))"
$lines += "- Deleted: $($result.artifacts.deletedArtifacts) ($(Format-GiB ([long]$result.artifacts.deletedBytes)))"
$lines += "- Failed deletes: $($result.artifacts.failedDeletes)"
}

if ($result.runner) {
$lines += ''
$lines += '#### Runner'
$lines += "- Free before: $(Format-GiB ([long]$result.runner.freeBytesBefore))"
$lines += "- Free after: $(Format-GiB ([long]$result.runner.freeBytesAfter))"
$lines += "- Aggressive cleanup: $(if ($result.runner.aggressiveApplied) { 'yes' } else { 'no' })"
}

Write-Host ("GitHub housekeeping: requested={0}; completed={1}; failed={2}" -f `
(@($result.requestedSections) -join ','), `
(@($result.completedSections) -join ','), `
(@($result.failedSections) -join ','))

Write-MarkdownSummary -Lines ($lines + '')
}

$configPath = Resolve-ConfigPath
if (-not (Test-Path -LiteralPath $configPath)) {
throw "Housekeeping config not found: $configPath"
}

$arguments = [System.Collections.Generic.List[string]]::new()
$arguments.AddRange(@(
'run', '--project', $project, '-c', 'Release', '--no-build', '--',
'github', 'housekeeping',
'--config', $configPath
))

if ($env:INPUT_APPLY -eq 'true') {
$null = $arguments.Add('--apply')
} else {
$null = $arguments.Add('--dry-run')
}

if (-not [string]::IsNullOrWhiteSpace($env:POWERFORGE_GITHUB_TOKEN)) {
$null = $arguments.Add('--token')
$null = $arguments.Add($env:POWERFORGE_GITHUB_TOKEN)
}

$null = $arguments.Add('--output')
$null = $arguments.Add('json')

$rawOutput = (& dotnet $arguments 2>&1 | Out-String).Trim()
$exitCode = $LASTEXITCODE

if ([string]::IsNullOrWhiteSpace($rawOutput)) {
if ($exitCode -ne 0) {
throw "PowerForge housekeeping failed with exit code $exitCode and produced no output."
}

return
}

try {
$envelope = $rawOutput | ConvertFrom-Json -Depth 30
} catch {
Write-Host $rawOutput
throw
}

Write-HousekeepingSummary -Envelope $envelope

if (-not $envelope.success) {
Write-Host $rawOutput
if ($envelope.exitCode) {
exit [int]$envelope.exitCode
}

exit 1
}
50 changes: 50 additions & 0 deletions .github/actions/github-housekeeping/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# PowerForge GitHub Housekeeping

Reusable composite action that runs the config-driven `powerforge github housekeeping` command from `PowerForge.Cli`.

## What it does

- Loads housekeeping settings from a repo config file, typically `.powerforge/github-housekeeping.json`
- Runs artifact cleanup, cache cleanup, and optional runner cleanup from one C# entrypoint
- Writes a workflow summary with the requested sections plus before/after cleanup stats

## Recommended usage

Use the reusable workflow for the leanest repo wiring:

```yaml
permissions:
contents: read
actions: write

jobs:
housekeeping:
uses: EvotecIT/PSPublishModule/.github/workflows/reusable-github-housekeeping.yml@main
with:
config-path: ./.powerforge/github-housekeeping.json
secrets: inherit
```

## Direct action usage

```yaml
permissions:
contents: read
actions: write

jobs:
housekeeping:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EvotecIT/PSPublishModule/.github/actions/github-housekeeping@main
with:
config-path: ./.powerforge/github-housekeeping.json
github-token: ${{ secrets.GITHUB_TOKEN }}
```

## Notes

- Cache and artifact deletion need `actions: write`.
- Set `apply: "false"` to preview without deleting anything.
- Hosted-runner repos should usually keep `runner.enabled` set to `false` in config.
44 changes: 44 additions & 0 deletions .github/actions/github-housekeeping/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: PowerForge GitHub Housekeeping
description: Run config-driven GitHub housekeeping using PowerForge.Cli.

inputs:
config-path:
description: Path to the housekeeping config file inside the checked-out repository.
required: false
default: ".powerforge/github-housekeeping.json"
apply:
description: When true, apply deletions. When false, run in dry-run mode.
required: false
default: "true"
github-token:
description: Optional token override for remote GitHub cleanup.
required: false
default: ""

runs:
using: composite
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: ${{ github.action_path }}/../../global.json

- name: Build PowerForge CLI
shell: pwsh
run: |
$repoRoot = (Resolve-Path (Join-Path $env:GITHUB_ACTION_PATH "../..")).Path
$project = Join-Path $repoRoot "PowerForge.Cli/PowerForge.Cli.csproj"
dotnet build $project -c Release
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: 1

- name: Run GitHub housekeeping
shell: pwsh
run: ${{ github.action_path }}/Invoke-PowerForgeHousekeeping.ps1
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: 1
INPUT_CONFIG_PATH: ${{ inputs['config-path'] }}
INPUT_APPLY: ${{ inputs.apply }}
POWERFORGE_GITHUB_TOKEN: ${{ inputs['github-token'] != '' && inputs['github-token'] || github.token }}
29 changes: 29 additions & 0 deletions .github/workflows/github-housekeeping.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: GitHub Housekeeping

on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
inputs:
apply:
description: 'Apply deletions (true/false)'
required: false
default: 'true'

permissions:
actions: write
contents: read

concurrency:
group: github-housekeeping-${{ github.repository }}
cancel-in-progress: false

jobs:
housekeeping:
uses: ./.github/workflows/reusable-github-housekeeping.yml
with:
config-path: ./.powerforge/github-housekeeping.json
apply: ${{ github.event_name == 'workflow_dispatch' && inputs.apply == 'true' || github.event_name != 'workflow_dispatch' }}
powerforge-ref: ${{ github.sha }}
secrets:
github-token: ${{ secrets.GITHUB_TOKEN }}
48 changes: 48 additions & 0 deletions .github/workflows/reusable-github-housekeeping.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Reusable GitHub Housekeeping

on:
workflow_call:
inputs:
config-path:
description: Path to the housekeeping config file in the caller repository.
required: false
default: ".powerforge/github-housekeeping.json"
type: string
apply:
description: Whether the run should apply deletions.
required: false
default: true
type: boolean
powerforge-ref:
description: PSPublishModule ref used to resolve the shared housekeeping action.
required: false
default: "main"
type: string
secrets:
github-token:
required: false

permissions:
actions: write
contents: read

jobs:
housekeeping:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- name: Checkout PSPublishModule
uses: actions/checkout@v4
with:
repository: EvotecIT/PSPublishModule
ref: ${{ inputs['powerforge-ref'] }}
path: .powerforge/pspublishmodule

- name: Run PowerForge housekeeping
uses: ./.powerforge/pspublishmodule/.github/actions/github-housekeeping
with:
config-path: ${{ inputs['config-path'] }}
apply: ${{ inputs.apply && 'true' || 'false' }}
github-token: ${{ secrets['github-token'] != '' && secrets['github-token'] || github.token }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ DocProject/Help/html

# Click-Once directory
publish/
!PowerForgeStudio.Domain/Publish/
!PowerForgeStudio.Domain/Publish/*.cs

# Publish Web Output
*.[Pp]ublish.xml
Expand Down
21 changes: 21 additions & 0 deletions .powerforge/github-housekeeping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://raw.githubusercontent.com/EvotecIT/PSPublishModule/main/Schemas/github.housekeeping.schema.json",
"repository": "EvotecIT/PSPublishModule",
"tokenEnvName": "GITHUB_TOKEN",
"dryRun": false,
"artifacts": {
"enabled": true,
"keepLatestPerName": 10,
"maxAgeDays": 7,
"maxDelete": 200
},
"caches": {
"enabled": true,
"keepLatestPerKey": 2,
"maxAgeDays": 14,
"maxDelete": 200
},
"runner": {
"enabled": false
}
}
Loading
Loading