Skip to content
Merged
54 changes: 54 additions & 0 deletions .github/skills/backport-changes/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: backport-changes
description: Analyze and backport changes from nightly to the release branch in dotnet/dotnet-docker.
user-invocable: true
disable-model-invocation: true
---

# Backporting Changes

## Workflow

1. **Get candidates for backport**: Run `pwsh scripts/Get-BackportPRs.ps1` to find PRs to backport.
2. **Analyze PRs** - classify each PR using the backport guidelines below and present the analysis table to the user
3. **Cherry-pick** - confirm the plan with the user, then run `git cherry-pick <commits>` (in the order they were merged).
- If any templates or manifests changed, regenerate Dockerfiles and READMEs. Confirm that the diff looks correct.
4. **Resolve conflicts** - follow the conflict resolution table below; stop and consult the user for anything not covered

## Backport guidelines

- Backport:
- New images - if ready for the main branch
- Removal of EOL images - end-of-life image cleanup
- Dockerfile/template changes - structural changes to how images are built
- Image component updates - MinGit, PowerShell, and other tools
- Infrastructure and tooling changes - build scripts, CI/CD updates
- Automated `eng/common` updates - standard engineering infrastructure
- Do not backport:
- Version-only updates for daily/preview builds (no Dockerfile changes)
- Changes already on the release branch
- Experimental or incomplete features
- Requires extra consideration:
- Daily builds of .NET or appliance images - only backport if they include Dockerfile changes beyond simple version updates.

## Output

After analyzing PRs, present results in a table:

```markdown
| PR | Title | Backport | Reason | Commit |
| --- | --- | --- | --- | --- |
| #1234 | Update PowerShell to X.Y.Z | ✅ Yes | Component update | 123abc... |
| #1235 | Daily build .NET X.Y.Z-preview.Y | ❌ No | Version-only update, no Dockerfile changes | 123abc... |
| #1236 | Add Ubuntu X.Y images | ✅ Yes | New images ready for release | 123abc... |
```

## Conflict resolution

| Conflicting files | Resolution |
| --- | --- |
| `src/*` | Regenerate Dockerfiles. |
| `READMEs` | Regenerate READMEs. |
| `manifest.json` | Take changes from nightly, ensure `latest` tags are on the correct (non-preview) versions, then regenerate Dockerfiles and READMEs. |
| `manifest.versions.json` | Keep the latest/most up-to-date versions. |
| Other files | Stop and consult the user. |
66 changes: 66 additions & 0 deletions .github/skills/backport-changes/scripts/Get-BackportPRs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env pwsh

# Lists pull requests with the 'needs-backport' label in dotnet/dotnet-docker.
# Separates PRs merged before the most recent Patch Tuesday (likely stale labels).

function Get-PatchTuesday ([int]$Offset = 0) {
$target = [datetime]::Today.AddMonths($Offset)
$firstOfMonth = [datetime]::new($target.Year, $target.Month, 1)
$firstTuesday = $firstOfMonth
while ($firstTuesday.DayOfWeek -ne [DayOfWeek]::Tuesday) {
$firstTuesday = $firstTuesday.AddDays(1)
}
return $firstTuesday.AddDays(7)
}

function Write-PRTable ($prs) {
Write-Host "| # | Title | Author | State | Created | Merged |"
Write-Host "|---|-------|--------|-------|---------|--------|"
foreach ($pr in $prs) {
$created = ([datetime]$pr.createdAt).ToString("yyyy-MM-dd")
$merged = if ($pr.mergedAt) { ([datetime]$pr.mergedAt).ToString("yyyy-MM-dd") } else { "" }
$state = $pr.state
$author = $pr.author.login
Write-Host "| #$($pr.number) | $($pr.title) | $author | $state | $created | $merged |"
}
}

$prs = gh pr list --repo dotnet/dotnet-docker --label needs-backport --state all --json number,title,state,createdAt,mergedAt,author --limit 100 | ConvertFrom-Json
$prs = $prs | Sort-Object createdAt

if ($prs.Count -eq 0) {
Write-Host "No PRs found with the 'needs-backport' label."
return
}

$patchTuesday = Get-PatchTuesday -1
$current = @()
$stale = @()

foreach ($pr in $prs) {
if ($pr.mergedAt -and ([datetime]$pr.mergedAt) -lt $patchTuesday) {
$stale += $pr
} else {
$current += $pr
}
}

Write-Host "## Needs Backport"
Write-Host ""
Write-Host "The following pull requests are candidates to be backported from the nightly branch to the release branch."
Write-Host ""
if ($current.Count -gt 0) {
Write-PRTable $current
} else {
Write-Host "_None._"
}

if ($stale.Count -gt 0) {
Write-Host ""
Write-Host "## Possibly Stale (merged before $($patchTuesday.ToString("yyyy-MM-dd")))"
Write-Host ""
Write-Host 'The following pull requests have the `needs-backport` label, but were merged before the most recent release.'
Write-Host 'They may already be backported but did not have their label removed.'
Write-Host ""
Write-PRTable $stale
}
23 changes: 23 additions & 0 deletions .github/skills/create-public-release-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: create-public-release-pr
description: Create a public PR to the dotnet-docker release branch with new .NET versions, and a follow-up PR from the release branch into main. Use when preparing the public release PR for a .NET containers servicing release.
user-invocable: true
disable-model-invocation: true
---

# Create Public Release PR

## Workflow

1. **Create a working branch** - create a new branch based off of the public release branch (e.g. `release-$ReleaseName`).
- Run `pwsh ../shared/Get-ReleaseBranches.ps1` to find the latest release branch.
- The most recently created branch corresponds to the current release.
2. **Collect stage container names** - read `stage-containers.txt` from the internal release branch get the stage container names for each release.
3. **Update Dockerfiles to new .NET versions** - for each .NET version to be released, run the update-dependencies tool from the root of the dotnet-docker repo:
```bash
dotnet run --project ./eng/update-dependencies/update-dependencies.csproj -- from-staging-pipeline $StageContainerName --azdo-organization "https://dev.azure.com/dnceng" --azdo-project internal --source-branch "main"
```
- Commit changes separately for each .NET version with this commit message format: `Update .NET X.0 to X.0.Y Runtime / X.0.ZZZ SDK`.
For previews: `Update .NET X.0 to X.0 Preview N`.
4. **Confirm with the user** — Let the user review the changes.
5. **Submit the PR** — Push the branch to `origin` and submit a PR to `main`.
30 changes: 30 additions & 0 deletions .github/skills/merge-main-to-nightly/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: merge-main-to-nightly
description: Create a PR to merge main into the nightly branch after a .NET containers servicing release. Use when updating the nightly branch post-release or merging main to nightly.
user-invocable: true
disable-model-invocation: true
---

# Merge main branch to nightly branch

## Workflow

1. **Determine the release name**: Run `pwsh ../shared/Get-ReleaseBranches.ps1` to find the latest release branch.
- The most recently created branch corresponds to the current release.
2. **Create a working branch** - create a new branch based off of the `nightly` branch, called `main-to-nightly-$releaseName`.
- `git fetch upstream nightly && git checkout -b main-to-nightly-$releaseName upstream/nightly`
3. **Merge main into the branch** - merge the `main` branch into your new branch, fixing any merge conflicts as necessary.
- `git fetch upstream main && git merge upstream/main`
- If there are merge conflicts, refer to the conflict resolution table below.
4. **Confirm with the user** - Let the user review the changes and the diff.
5. **Submit the PR** - Push the branch to `origin` and create a PR targeting the `nightly` branch.

## Conflict resolution

| Conflicting files | Resolution |
|---|---|
| `src/*` | Regenerate Dockerfiles. |
| `READMEs` | Regenerate READMEs. |
| `manifest.json` | Take changes from nightly, ensure preview/nightly tags remain, then regenerate Dockerfiles and READMEs. |
| `manifest.versions.json` | Keep nightly's versions (which are typically newer/preview). Incorporate any structural changes from main. |
| Other files | Stop and consult the user. |
64 changes: 64 additions & 0 deletions .github/skills/shared/Get-ReleaseBranches.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env pwsh

# Finds the latest public and internal release branches in dotnet/dotnet-docker
# by fetching from GitHub (public) and Azure DevOps (internal) and listing by
# branch creation date.

function Get-RemoteName {
param(
[Parameter(Mandatory = $true)]
[string] $UrlPattern
)

$matchingRemotes = @(git remote | Where-Object {
$remoteName = $_
$remoteUrl = git remote get-url $remoteName 2>$null
$remoteUrl -match $UrlPattern
})

if ($matchingRemotes.Count -eq 0) {
throw "Unable to find a remote with a URL matching '$UrlPattern'."
}

if ($matchingRemotes.Count -gt 1) {
throw "Found multiple remotes with URLs matching '$UrlPattern': $($matchingRemotes -join ', ')."
}

return $matchingRemotes[0]
}

# Show basic documentation
Write-Host "# Release branches"
Write-Host "Release branches follow the Windows release naming scheme."
Write-Host "Example: 2026-04B refers to the second week (B) of April 2026."

$gitHubRemote = Get-RemoteName -UrlPattern 'github\.com[:/]dotnet/'
$dncengRemote = Get-RemoteName -UrlPattern 'dev\.azure\.com/dnceng/'
$upstreamRemoteUrl = git remote get-url $gitHubRemote
$dncengRemoteUrl = git remote get-url $dncengRemote

Write-Host ""
Write-Host "## Repo configuration"
Write-Host "This repo is configured with the following remotes:"
Write-Host ""
Write-Host "Source | Remote Name | Remote URL"
Write-Host "--- | --- | ---"
Write-Host "Public | $gitHubRemote | $upstreamRemoteUrl"
Write-Host "Internal | $dncengRemote | $dncengRemoteUrl"

git fetch $gitHubRemote 2>&1 | Out-Null
git fetch $dncengRemote 2>&1 | Out-Null

$numberOfBranches = 5

Write-Host ""
Write-Host "## ${numberOfBranches} most recent public release branches"
git branch -r --list "$gitHubRemote/release/*" --sort=-creatordate `
| Select-Object -First $numberOfBranches `
| ForEach-Object { Write-Host "- $($_.Trim())" }

Write-Host ""
Write-Host "## ${numberOfBranches} most recent internal release branches"
git branch -r --list "$dncengRemote/internal/release/*" --sort=-creatordate `
| Select-Object -First $numberOfBranches `
| ForEach-Object { Write-Host "- $($_.Trim())" }
Loading