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
9 changes: 8 additions & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,19 @@ The "Release" workflow downloads the artifacts from a [Package](#package) workfl

The following actions are taken:

- Build containers for Devolutions Gateway and publish to Docker
- Build multi-architecture containers for Devolutions Gateway (AMD64 and ARM64) and publish to Docker
- Push the Devolutions Gateway PowerShell module to PSGallery
- Generate a GitHub release

Re-releasing the same version multiple times is not supported. The "Release" workflow checks for an existing GitHub release with the specified version and will not proceed if found.

##### Multi-Architecture Docker Images

Devolutions Gateway Docker images support both AMD64 (x86_64) and ARM64 (aarch64) architectures. The workflow:
1. Prepares separate build contexts for each architecture with appropriate binaries and native libraries
2. Uses Docker Buildx and QEMU to build both architectures on x86_64 runners
3. Creates multi-arch manifest lists so users automatically get the correct image for their platform

##### Parameters

- `run` The run-id of the [Package](#package) workflow run containing the artifacts to package
Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,38 @@ jobs:
echo "version=$Version" >> $Env:GITHUB_OUTPUT
shell: pwsh

# Download Cadeau native libraries for all supported platforms and architectures.
# Cadeau provides the libxmf library for XMF (eXtensible Media Format) support.
#
# Organization strategy:
# We organize native libraries by platform and architecture (native-libs/{platform}/{arch}/)
# because different architectures require different compiled binaries even on the same OS.
# This structure makes it explicit which library is used for each platform+arch combination,
# preventing accidental use of wrong-architecture libraries which would cause runtime failures.
#
# Structure:
# native-libs/windows/x64/xmf.dll - Windows x64
# native-libs/linux/x64/libxmf.so - Linux x86_64 (amd64)
# native-libs/linux/arm64/libxmf.so - Linux ARM64 (aarch64)
- name: Download Cadeau
run: |
# Download Windows x64
./ci/download-cadeau.ps1 -Platform 'win' -Architecture 'x64'
$WinFiles = Get-ChildItem -Path native-libs -File
New-Item -ItemType Directory -Path native-libs/windows/x64 -Force | Out-Null
$WinFiles | Move-Item -Destination native-libs/windows/x64

# Download Linux x64
./ci/download-cadeau.ps1 -Platform 'linux' -Architecture 'x64'
$LinuxX64Files = Get-ChildItem -Path native-libs -File
New-Item -ItemType Directory -Path native-libs/linux/x64 -Force | Out-Null
$LinuxX64Files | Move-Item -Destination native-libs/linux/x64

# Download Linux arm64
./ci/download-cadeau.ps1 -Platform 'linux' -Architecture 'arm64'
$LinuxArm64Files = Get-ChildItem -Path native-libs -File
New-Item -ItemType Directory -Path native-libs/linux/arm64 -Force | Out-Null
$LinuxArm64Files | Move-Item -Destination native-libs/linux/arm64
shell: pwsh

- name: Upload native libs
Expand Down Expand Up @@ -391,7 +419,7 @@ jobs:
$Env:DGATEWAY_PSMODULE_PATH = Join-Path $PackageRoot PowerShell DevolutionsGateway
$Env:DGATEWAY_WEBCLIENT_PATH = Join-Path "webapp" "client" | Resolve-Path
$Env:DGATEWAY_WEBPLAYER_PATH = Join-Path "webapp" "player" | Resolve-Path
$Env:DGATEWAY_LIB_XMF_PATH = Join-Path "native-libs" "xmf.dll" | Resolve-Path
$Env:DGATEWAY_LIB_XMF_PATH = Join-Path "native-libs" "windows" "x64" "xmf.dll" | Resolve-Path

Write-Host "DGATEWAY_EXECUTABLE = ${Env:DGATEWAY_EXECUTABLE}"
Write-Host "DGATEWAY_PSMODULE_PATH = ${Env:DGATEWAY_PSMODULE_PATH}"
Expand Down
175 changes: 144 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: [x86_64]
os: [linux]
base-image: [bookworm-slim]

Expand All @@ -127,17 +126,22 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Prepare artifacts
id: prepare-artifacts
# Multi-arch Docker build strategy:
# We prepare separate build contexts for amd64 and arm64 because each architecture
# needs its own pre-compiled binary and architecture-specific native libraries (libxmf.so).
# This approach is more reliable than trying to use COPY --platform in the Dockerfile,
# which would require the binaries to be organized in a specific directory structure.
- name: Prepare artifacts (amd64)
id: prepare-artifacts-amd64
run: |
Set-PSDebug -Trace 1

$PkgDir = Join-Path docker $Env:RUNNER_OS # RUNNER_OS is camelcase
echo "package-path=$PkgDir" >> $Env:GITHUB_OUTPUT
$PkgDir = Join-Path docker $Env:RUNNER_OS "amd64"
echo "package-path-amd64=$PkgDir" >> $Env:GITHUB_OUTPUT
Write-Host "PkgDir = $PkgDir"
Get-ChildItem -Path "$PkgDir"
New-Item -ItemType Directory -Path $PkgDir -Force

$SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_${{ matrix.arch }}"
$SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_x86_64"
$TargetFileName = "devolutions-gateway"
Write-Host "SourceFileName = $SourceFileName"
Write-Host "TargetFileName = $TargetFileName"
Expand All @@ -147,13 +151,10 @@ jobs:
Write-Host "SourcePath = $SourcePath"
Write-Host "TargetPath = $TargetPath"
Copy-Item -Path $SourcePath -Destination $TargetPath

if ($Env:RUNNER_OS -eq "Linux") {
Invoke-Expression "chmod +x $TargetPath"
}
chmod +x $TargetPath

$XmfFileName = "libxmf.so"
$XmfSourcePath = Get-ChildItem -Recurse -Filter $XmfFileName -File -Path native-libs
$XmfSourcePath = Join-Path "native-libs" "linux" "x64" $XmfFileName | Resolve-Path
$XmfTargetPath = Join-Path $PkgDir $XmfFileName
Write-Host "XmfSourcePath = $XmfSourcePath"
Write-Host "XmfTargetPath = $XmfTargetPath"
Expand All @@ -170,42 +171,154 @@ jobs:
$psModuleArchiveHash = (Get-FileHash -Path "$PowerShellArchive").Hash
Write-Host "PS module archive hash: $psModuleArchiveHash"
tar -xvf "$PowerShellArchive" -C "$PkgDir"

# Copy Dockerfile and entrypoint
Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir
Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir
Comment on lines +176 to +177
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect path to Dockerfile and entrypoint.ps1. The 'docker' artifact uploaded in package.yml contains files at package/Linux/Dockerfile and package/Linux/entrypoint.ps1, which when downloaded will be at docker/package/Linux/Dockerfile and docker/package/Linux/entrypoint.ps1, not docker/Linux/Dockerfile. This path should be "docker/package/Linux/Dockerfile" and "docker/package/Linux/entrypoint.ps1".

Suggested change
Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir
Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir
Copy-Item -Path "docker/package/Linux/Dockerfile" -Destination $PkgDir
Copy-Item -Path "docker/package/Linux/entrypoint.ps1" -Destination $PkgDir

Copilot uses AI. Check for mistakes.
shell: pwsh

- name: Build container
id: build-container
- name: Prepare artifacts (arm64)
id: prepare-artifacts-arm64
run: |
Set-PSDebug -Trace 1

$Version = "${{ needs.preflight.outputs.version }}"
$ImageName = "devolutions/devolutions-gateway:$Version"
$LatestImageName = "devolutions/devolutions-gateway:latest"
$PkgDir = Join-Path docker $Env:RUNNER_OS "arm64"
echo "package-path-arm64=$PkgDir" >> $Env:GITHUB_OUTPUT
Write-Host "PkgDir = $PkgDir"
New-Item -ItemType Directory -Path $PkgDir -Force

$SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_arm64"
$TargetFileName = "devolutions-gateway"
Write-Host "SourceFileName = $SourceFileName"
Write-Host "TargetFileName = $TargetFileName"

$SourcePath = Get-ChildItem -Recurse -Filter $SourceFileName -File -Path devolutions-gateway
$TargetPath = Join-Path $PkgDir $TargetFileName
Write-Host "SourcePath = $SourcePath"
Write-Host "TargetPath = $TargetPath"
Copy-Item -Path $SourcePath -Destination $TargetPath
chmod +x $TargetPath

$XmfFileName = "libxmf.so"
$XmfSourcePath = Join-Path "native-libs" "linux" "arm64" $XmfFileName | Resolve-Path
$XmfTargetPath = Join-Path $PkgDir $XmfFileName
Write-Host "XmfSourcePath = $XmfSourcePath"
Write-Host "XmfTargetPath = $XmfTargetPath"
Copy-Item -Path $XmfSourcePath -Destination $XmfTargetPath

$WebAppArchive = Get-ChildItem -Recurse -Filter "devolutions_gateway_webapp_*.tar.gz" | Select-Object -First 1
$TargetPath = Join-Path $PkgDir "webapp" "client"
Write-Host "WebAppArchive = $WebAppArchive"
Write-Host "TargetPath = $TargetPath"
New-Item -ItemType Directory -Path $TargetPath
tar -xvzf $WebAppArchive.FullName -C $TargetPath --strip-components=1

docker build -t "$ImageName" -t "$LatestImageName" .
echo "image-name=$ImageName" >> $Env:GITHUB_OUTPUT
echo "latest-image-name=$LatestImageName" >> $Env:GITHUB_OUTPUT
$PowerShellArchive = Get-ChildItem -Recurse -Filter "DevolutionsGateway-ps-*.tar" | Select-Object -First 1
$psModuleArchiveHash = (Get-FileHash -Path "$PowerShellArchive").Hash
Write-Host "PS module archive hash: $psModuleArchiveHash"
tar -xvf "$PowerShellArchive" -C "$PkgDir"

Get-ChildItem -Recurse
# Copy Dockerfile and entrypoint
Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir
Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir
Comment on lines +222 to +223
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect path to Dockerfile and entrypoint.ps1. The 'docker' artifact uploaded in package.yml contains files at package/Linux/Dockerfile and package/Linux/entrypoint.ps1, which when downloaded will be at docker/package/Linux/Dockerfile and docker/package/Linux/entrypoint.ps1, not docker/Linux/Dockerfile. This path should be "docker/package/Linux/Dockerfile" and "docker/package/Linux/entrypoint.ps1".

Suggested change
Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir
Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir
Copy-Item -Path "docker/package/Linux/Dockerfile" -Destination $PkgDir
Copy-Item -Path "docker/package/Linux/entrypoint.ps1" -Destination $PkgDir

Copilot uses AI. Check for mistakes.
shell: pwsh
working-directory: ${{ steps.prepare-artifacts.outputs.package-path }}

- name: Push container
# QEMU is required for cross-platform Docker builds on x86_64 runners.
# It enables the runner to emulate ARM64 architecture during the build process.
# Without QEMU, we would need native ARM64 runners (which are more expensive and less available).
# Note: QEMU is only used during the IMAGE BUILD, not at runtime - the final images are native.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

# Docker Buildx is required for multi-platform builds and creating manifest lists.
# It provides:
# 1. The ability to build for multiple architectures in a single command
# 2. The 'docker buildx imagetools' command for creating multi-arch manifests
# 3. Better caching and build performance compared to legacy docker build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: devolutionsbot
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

# Multi-arch build strategy:
# 1. Build separate images for each architecture with arch-specific tags
# 2. Push each image to the registry
# 3. Create a multi-arch manifest that references both images
#
# When users pull the image without specifying architecture, Docker automatically
# selects the correct variant based on their platform. This works because:
# - Each image is tagged with the architecture (e.g., :2025.3.3-amd64)
# - The manifest list (:2025.3.3) contains references to both arch-specific images
# - Docker client inspects the manifest and pulls the matching architecture
#
# Why we use separate contexts instead of --platform linux/amd64,linux/arm64:
# - Each architecture needs different pre-compiled binaries and native libraries
# - Separate contexts allow us to use architecture-specific artifacts directly
# - More explicit and easier to debug than complex Dockerfile conditionals
- name: Build and push multi-arch container
id: build-container
run: |
Set-PSDebug -Trace 1

echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u devolutionsbot --password-stdin
$DockerPushCmd = 'docker push ${{ steps.build-container.outputs.image-name }}'
$DockerPushLatestCmd = 'docker push ${{ steps.build-container.outputs.latest-image-name }}'
Write-Host $DockerPushCmd
Write-Host $DockerPushLatestCmd
$Version = "${{ needs.preflight.outputs.version }}"
$ImageName = "devolutions/devolutions-gateway"

$Amd64Context = "${{ steps.prepare-artifacts-amd64.outputs.package-path-amd64 }}"
$Arm64Context = "${{ steps.prepare-artifacts-arm64.outputs.package-path-arm64 }}"

$DryRun = [System.Convert]::ToBoolean('${{ inputs.dry-run }}')

# Build and push amd64 image
Write-Host "Building amd64 image..."
if (-Not $DryRun) {
docker buildx build --platform linux/amd64 `
--tag "${ImageName}:${Version}-amd64" `
--push `
$Amd64Context
} else {
docker buildx build --platform linux/amd64 `
--tag "${ImageName}:${Version}-amd64" `
$Amd64Context
}
Comment on lines +278 to +286
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker buildx build commands lack explicit error handling. If a build fails, the script will continue to the next steps. Consider adding error checking after each docker command (e.g., if ($LASTEXITCODE -ne 0) { throw "Build failed" }) to ensure failures are caught and the workflow stops appropriately.

Copilot uses AI. Check for mistakes.

# Build and push arm64 image (cross-compiled on x86_64 runner using QEMU)
Write-Host "Building arm64 image..."
if (-Not $DryRun) {
Invoke-Expression $DockerPushCmd
Invoke-Expression $DockerPushLatestCmd
docker buildx build --platform linux/arm64 `
--tag "${ImageName}:${Version}-arm64" `
--push `
$Arm64Context
} else {
docker buildx build --platform linux/arm64 `
--tag "${ImageName}:${Version}-arm64" `
$Arm64Context
}
Comment on lines +291 to +299
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker buildx build commands lack explicit error handling. If a build fails, the script will continue to the next steps. Consider adding error checking after each docker command (e.g., if ($LASTEXITCODE -ne 0) { throw "Build failed" }) to ensure failures are caught and the workflow stops appropriately.

Copilot uses AI. Check for mistakes.

if (-Not $DryRun) {
# Create multi-arch manifests that reference both architecture-specific images.
# This enables Docker to automatically select the correct image based on the user's platform.
Write-Host "Creating multi-arch manifest for version ${Version}..."
docker buildx imagetools create `
--tag "${ImageName}:${Version}" `
"${ImageName}:${Version}-amd64" `
"${ImageName}:${Version}-arm64"

Write-Host "Creating multi-arch manifest for latest..."
docker buildx imagetools create `
--tag "${ImageName}:latest" `
"${ImageName}:${Version}-amd64" `
"${ImageName}:${Version}-arm64"
Comment on lines +309 to +314
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker buildx imagetools create commands lack explicit error handling. If manifest creation fails, the script will continue. Consider adding error checking after each command (e.g., if ($LASTEXITCODE -ne 0) { throw "Manifest creation failed" }) to ensure failures are caught and the workflow stops appropriately.

Suggested change
Write-Host "Creating multi-arch manifest for latest..."
docker buildx imagetools create `
--tag "${ImageName}:latest" `
"${ImageName}:${Version}-amd64" `
"${ImageName}:${Version}-arm64"
if ($LASTEXITCODE -ne 0) { throw "Manifest creation for version ${Version} failed" }
Write-Host "Creating multi-arch manifest for latest..."
docker buildx imagetools create `
--tag "${ImageName}:latest" `
"${ImageName}:${Version}-arm64"
if ($LASTEXITCODE -ne 0) { throw "Manifest creation for latest failed" }

Copilot uses AI. Check for mistakes.
} else {
Write-Host "Dry run: skipping manifest creation and push"
}

echo "image-name=${ImageName}:${Version}" >> $Env:GITHUB_OUTPUT
echo "latest-image-name=${ImageName}:latest" >> $Env:GITHUB_OUTPUT
shell: pwsh
working-directory: ${{ steps.prepare-artifacts.outputs.package-path }}

github-release:
name: GitHub release
Expand Down
26 changes: 19 additions & 7 deletions package/Linux/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
FROM debian:bookworm-slim
LABEL maintainer="Devolutions Inc."

# Install PowerShell and dependencies
# Microsoft's APT repository doesn't have PowerShell for ARM64, so we install from GitHub releases
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget ca-certificates \
&& wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \
&& dpkg -i packages-microsoft-prod.deb \
&& rm packages-microsoft-prod.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
powershell openssl \
&& apt-get install -y --no-install-recommends wget ca-certificates openssl \
&& ARCH=$(dpkg --print-architecture) \
&& if [ "$ARCH" = "arm64" ]; then \
PWSH_VERSION=7.4.6 \
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded PowerShell version 7.4.6 creates a maintenance burden and could lead to version drift between architectures. Consider using a variable or ARG at the top of the Dockerfile (e.g., ARG PWSH_VERSION=7.4.6) to make version updates easier and more consistent.

Copilot uses AI. Check for mistakes.
&& wget -q "https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-arm64.tar.gz" \
&& mkdir -p /opt/microsoft/powershell/7 \
&& tar -xzf "powershell-${PWSH_VERSION}-linux-arm64.tar.gz" -C /opt/microsoft/powershell/7 \
&& chmod +x /opt/microsoft/powershell/7/pwsh \
&& ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \
&& rm "powershell-${PWSH_VERSION}-linux-arm64.tar.gz"; \
else \
wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \
&& dpkg -i packages-microsoft-prod.deb \
&& rm packages-microsoft-prod.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends powershell; \
fi \
&& rm -rf /var/lib/apt/lists/*

ENV XDG_CACHE_HOME="/tmp/.cache"
Expand Down
Loading