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
118 changes: 118 additions & 0 deletions .github/actions/determine-image-tags/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Determine image tags
description: Determine the image tags for this release
inputs:
version:
description: Full version
required: true
outputs:
major:
description: Major version
value: ${{ steps.determine.outputs.major }}
minor:
description: Minor version
value: ${{ steps.determine.outputs.minor }}
patch:
description: Patch version
value: ${{ steps.determine.outputs.patch }}
prerelease:
description: Prerelease label
value: ${{ steps.determine.outputs.prerelease }}
image-tags:
description: Tags to be added for image releases (comma-separated)
value: ${{ steps.determine.outputs.image-tags }}
latest:
description: Evaluates to `true` if the version should be flagged as the new latest version
value: ${{ steps.determine.outputs.latest }}
runs:
using: composite
steps:
- name: Determine image tags
id: determine
shell: pwsh
run: |
$version = "${{ inputs.version }}"

Write-Output "Received version: $version"
$isMatch = $version -match '^(?<Major>\d+)\.(?<Minor>\d+)\.(?<Patch>\d+)(-(?<PrereleaseLabel>[\w\-\.]+))?$'
if ( -not $isMatch)
{
throw "Invalid version $version"
exit 1
}

Write-Output "Version $version is valid."

$major = [int]$Matches.Major
$minor = [int]$Matches.Minor
$patch = [int]$Matches.Patch
$prereleaseLabel = $Matches.PrereleaseLabel
$isPrerelease = -not -not $prereleaseLabel

echo "major=$major" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append
echo "minor=$minor" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append
echo "patch=$patch" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append
echo "prerelease=$prereleaseLabel" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append

# Get highest-versioned releases
$repository = "${{ github.repository }}"
$releases = Invoke-WebRequest -Uri "https://api.github.com/repos/$repository/releases" -UseBasicParsing |
ConvertFrom-Json -ErrorAction Stop
$releasedVersions = $releases |
where Draft -eq $false |
where Prerelease -eq $false |
where Name -match '^\d+\.\d+\.\d+$' | # Ignore prerelease tags
select -ExpandProperty tag_name

$latestVersion = $releasedVersions |
Sort-Object { [System.Version]$_ } -Descending |
select -First 1
Write-Output "Latest released version on GitHub = $latestVersion"

$latestForMajor = $releasedVersions |
where { ([System.Version]$_).Major -eq $major } |
Sort-Object { [System.Version]$_ } -Descending |
select -First 1
Write-Output "Latest released v$major version on GitHub = $latestForMajor"

$latestForMinor = $releasedVersions |
where { ([System.Version]$_).Major -eq $major -and ([System.Version]$_).Minor -eq $minor } |
Sort-Object { [System.Version]$_ } -Descending |
select -First 1
Write-Output "Latest released v$major.$minor version on GitHub = $latestForMinor"

Write-Output "Determining image tags..."
$tags = @($version)

$isLatest = $false
if ( -not $isPrerelease ) {
$vNew = [System.Version]$version
$vPrev = [System.Version]$latestVersion
$vPrevForMajor = [System.Version]$latestForMajor
$vPrevForMinor = [System.Version]$latestForMinor

if ($vNew -ge $vPrev) {
$isLatest = $true
$tags += 'latest'
}

# $vPrevForMajor is $null for a brand-new major; vNew -ge $null evaluates to true, so the floating
# major tag is applied. Otherwise, only move it forward when this version is the newest for its major
# (skips it for a backport/patch to an older major tag).
if ($vNew -ge $vPrevForMajor) {
$tags += "$major"
}

# Same reasoning as above, but for the minor tag.
if ($vNew -ge $vPrevForMinor) {
$tags += "$major.$minor"
}
}

Write-Output "Major = $major, Minor = $minor, Patch = $patch, PrereleaseLabel = $prereleaseLabel, IsPrerelease = $isPrerelease, IsLatest = $isLatest"

Write-Output "Tags to apply:"
$tags | ForEach-Object { Write-Output " * '$_'" }

$tagsDelimited = $tags -Join ','
echo "image-tags=$tagsDelimited" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append
echo "latest=$($isLatest.ToString().ToLower())" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append
121 changes: 117 additions & 4 deletions .github/workflows/push-docker-images.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,125 @@
name: Push Docker Images
on:
workflow_call:
inputs:
version:
description: Full version of container images to push.
required: true
type: string
image-config-file:
description: >
Path (relative to the repository root) to a JSON file listing images to push, each with `name`,
`readme_filepath`, and `short_description`. `name` is used both as the GHCR source image and as the
`particular/<name>` Docker Hub repository.
required: false
type: string
default: .github/docker-images.json
permissions:
contents: read
packages: write
jobs:
push-docker-images:
determine-image-configurations:
runs-on: ubuntu-latest
name: Determine image configurations
defaults:
run:
shell: bash
shell: pwsh
outputs:
image-tags: ${{ steps.tags.outputs.image-tags }}
latest: ${{ steps.tags.outputs.latest }}
image-configs: ${{ steps.parse-image-config.outputs.image-configs }}
steps:
- name: Placeholder
run: echo "Placeholder for pushing Docker images"
- name: Checkout
uses: actions/checkout@v7.0.1
- name: Determine image tags
id: tags
uses: $/.github/actions/determine-image-tags
with:
version: ${{ inputs.version }}
- name: Parse and validate image config
id: parse-image-config
run: |
$path = "${{ inputs.image-config-file }}"

if ( -not (Test-Path $path) )
{
throw "image-config-file '$path' was not found in the repository."
}

$imageConfigs = Get-Content -Raw -Path $path | ConvertFrom-Json -ErrorAction Stop

if ($imageConfigs -isnot [array] -or $imageConfigs.Count -eq 0)
{
throw "image-config-file '$path' must contain a non-empty JSON array."
}

foreach ($imageConfig in $imageConfigs)
{
foreach ($field in @('name', 'readme_filepath', 'short_description'))
{
if ([string]::IsNullOrWhiteSpace($imageConfig.$field))
{
throw "image-config-file '$path' has an entry missing required field '$field': $($imageConfig | ConvertTo-Json -Compress)"
}
}
}

# -Compress keeps this on one line; $GITHUB_OUTPUT is line-based and a pretty-printed (multi-line) value would corrupt it.
$imageConfigsJson = $imageConfigs | ConvertTo-Json -Compress -AsArray
echo "image-configs=$imageConfigsJson" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append
push-to-container-registry:
needs: determine-image-configurations
runs-on: ubuntu-latest
name: Push to container registry
defaults:
run:
shell: pwsh
steps:
- name: Log in to GitHub container registry
uses: docker/login-action@v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4.1.0
- name: Publish to Docker Hub
run: |
$imageConfigs = '${{ needs.determine-image-configurations.outputs.image-configs }}' | ConvertFrom-Json
$tags = "${{ needs.determine-image-configurations.outputs.image-tags }}" -Split ','
$sourceTag = "${{ inputs.version }}"

foreach ($imageConfig in $imageConfigs)
{
$name = $imageConfig.name
Write-Output "::group::Pushing $name with $tags tags"
$tagsCLI = $tags -replace "^", "--tag particular/${name}:"
$cmd = "docker buildx imagetools create $tagsCLI ghcr.io/particular/${name}:$sourceTag"
Write-Output "Command: $cmd"
Invoke-Expression $cmd
Write-Output "::endgroup::"
}
update-descriptions:
needs: [determine-image-configurations, push-to-container-registry]
if: ${{ needs.determine-image-configurations.outputs.latest == 'true' }}
runs-on: ubuntu-latest
name: Update Docker Hub description
strategy:
matrix:
image: ${{ fromJSON(needs.determine-image-configurations.outputs.image-configs) }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
- name: Update Docker Hub Description - ${{ matrix.image.name }}
uses: peter-evans/dockerhub-description@v5.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: particular/${{ matrix.image.name }}
readme-filepath: ${{ matrix.image.readme_filepath }}
short-description: ${{ matrix.image.short_description }}
Loading