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
48 changes: 40 additions & 8 deletions .github/actions/build-and-publish-image/action.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
name: Build and Publish Image
description: Steps for building an image for a specific ruby version
inputs:
publish_manifest:
required: false
default: "false"
description: "Set to true to publish a multi-arch manifest instead of building"
ruby_version:
required: true
description: "The version of Ruby to build the image for"
build_platform:
required: false
description: "The single platform to build (linux/amd64 or linux/arm64)"
platform_suffix:
required: false
description: "Platform suffix for arch-specific tags (amd64 or arm64)"
image_tag:
required: true
description: "The tag to use for the image"
Expand All @@ -16,18 +26,31 @@ inputs:
runs:
using: "composite"
steps:
- name: Validate required inputs
shell: bash
run: |
if [[ "${{ inputs.publish_manifest }}" != "true" ]]; then
if [[ -z "${{ inputs.build_platform }}" || -z "${{ inputs.platform_suffix }}" ]]; then
echo "build_platform and platform_suffix are required when publish_manifest is false"
exit 1
fi
fi

- name: Checkout (GitHub)
if: ${{ inputs.publish_manifest != 'true' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.image_tag }}

- name: Set up QEMU for multi-architecture builds
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
if: ${{ inputs.publish_manifest != 'true' }}
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
platforms: ${{ inputs.build_platform }}

- name: Set up Docker Buildx (manifest)
if: ${{ inputs.publish_manifest == 'true' }}
uses: docker/setup-buildx-action@v3

- name: Set Image version env variable
run: echo "IMAGE_VERSION=$(echo ${{ inputs.image_tag }} | tr -d ruby-)" >> $GITHUB_ENV
Expand All @@ -41,16 +64,25 @@ runs:
password: ${{ inputs.gh_token }}

- name: Pre-build Dev Container Image
if: ${{ inputs.publish_manifest != 'true' }}
uses: devcontainers/ci@v0.3
env:
RUBY_VERSION: ${{ inputs.ruby_version }}
BUILDX_NO_DEFAULT_ATTESTATIONS: true
with:
imageName: ghcr.io/rails/devcontainer/images/ruby
imageTag: ${{ env.IMAGE_VERSION }}-${{ inputs.ruby_version }},${{ inputs.ruby_version }}
imageTag: ${{ env.IMAGE_VERSION }}-${{ inputs.ruby_version }}-${{ inputs.platform_suffix }},${{ inputs.ruby_version }}-${{ inputs.platform_suffix }}
cacheFrom: ghcr.io/rails/devcontainer/images/ruby:${{ inputs.ruby_version }}-${{ inputs.platform_suffix }}
subFolder: images/ruby
push: always
platform: linux/amd64,linux/arm64
platform: ${{ inputs.build_platform }}

- name: Checkout (GitHub)
uses: actions/checkout@v6
- name: Create and push multi-arch manifest
if: ${{ inputs.publish_manifest == 'true' }}
shell: bash
run: |
docker buildx imagetools create \
-t ghcr.io/rails/devcontainer/images/ruby:${IMAGE_VERSION}-${{ inputs.ruby_version }} \
-t ghcr.io/rails/devcontainer/images/ruby:${{ inputs.ruby_version }} \
ghcr.io/rails/devcontainer/images/ruby:${IMAGE_VERSION}-${{ inputs.ruby_version }}-amd64 \
ghcr.io/rails/devcontainer/images/ruby:${IMAGE_VERSION}-${{ inputs.ruby_version }}-arm64
77 changes: 77 additions & 0 deletions .github/workflows/publish-images-reusable.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Publish Images Reusable

on:
workflow_call:
inputs:
ruby_versions_json:
required: true
type: string
description: JSON array of Ruby versions
image_versions_json:
required: true
type: string
description: JSON array of image tags
repository_owner:
required: true
type: string
description: Repository owner for GHCR login
secrets:
gh_token:
required: true

defaults:
run:
shell: bash

jobs:
build:
name: Build Images
strategy:
fail-fast: false
matrix:
RUBY_VERSION: ${{ fromJSON(inputs.ruby_versions_json) }}
IMAGE_VERSION: ${{ fromJSON(inputs.image_versions_json) }}
TARGET:
- RUNNER: ubuntu-24.04
BUILD_PLATFORM: linux/amd64
PLATFORM_SUFFIX: amd64
- RUNNER: ubuntu-24.04-arm
BUILD_PLATFORM: linux/arm64
PLATFORM_SUFFIX: arm64

runs-on: ${{ matrix.TARGET.RUNNER }}
permissions:
contents: read
packages: write
steps:
- name: Build and Publish Image
uses: ./.github/actions/build-and-publish-image
with:
ruby_version: ${{ matrix.RUBY_VERSION }}
build_platform: ${{ matrix.TARGET.BUILD_PLATFORM }}
platform_suffix: ${{ matrix.TARGET.PLATFORM_SUFFIX }}
image_tag: ${{ matrix.IMAGE_VERSION }}
gh_token: ${{ secrets.gh_token }}
repository_owner: ${{ inputs.repository_owner }}

publish-manifests:
name: Publish Multi-Arch Manifests
needs: build
strategy:
fail-fast: false
matrix:
RUBY_VERSION: ${{ fromJSON(inputs.ruby_versions_json) }}
IMAGE_VERSION: ${{ fromJSON(inputs.image_versions_json) }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- name: Publish Multi-Arch Manifest
uses: ./.github/actions/build-and-publish-image
with:
publish_manifest: "true"
ruby_version: ${{ matrix.RUBY_VERSION }}
image_tag: ${{ matrix.IMAGE_VERSION }}
gh_token: ${{ secrets.gh_token }}
repository_owner: ${{ inputs.repository_owner }}
28 changes: 10 additions & 18 deletions .github/workflows/publish-new-image-version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,24 @@ name: Build and Publish Images
- ruby-*.*.*
jobs:
setup:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- id: set-matrix
run: echo "matrix=$(cat .github/ruby-versions.json | jq -c '.')" >> $GITHUB_OUTPUT

build:
name: Build Images
publish:
name: Publish Images
needs: setup
strategy:
fail-fast: false
matrix:
RUBY_VERSION: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ubuntu-latest
uses: ./.github/workflows/publish-images-reusable.yaml
with:
ruby_versions_json: ${{ needs.setup.outputs.matrix }}
image_versions_json: ${{ format('["{0}"]', github.ref_name) }}
repository_owner: ${{ github.repository_owner }}
secrets:
gh_token: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: write
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v6
- name: Build and Publish Image
uses: ./.github/actions/build-and-publish-image
with:
ruby_version: ${{ matrix.RUBY_VERSION }}
image_tag: ${{ github.ref_name }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
repository_owner: ${{ github.repository_owner }}
55 changes: 33 additions & 22 deletions .github/workflows/publish-new-ruby-versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,45 @@ on:
ruby_versions:
type: string
required: true
description: List of ruby versions to build. Should be an array ["3.3.1","3.2.4"]
description: Comma or newline-separated Ruby versions (example: 3.3.1, 3.2.4)
image_versions:
type: string
required: true
description: List of image versions to build. Should be an array ["ruby-1.1.0"]
required: false
description: Optional comma or newline-separated image versions (example: 1.1.0, 1.2.0 or ruby-1.1.0). If empty, latest ruby-* tag is used.

jobs:
build:
name: Build Images

strategy:
fail-fast: false
matrix:
RUBY_VERSION: ${{ fromJSON(github.event.inputs.ruby_versions)}}
IMAGE_VERSION: ${{ fromJSON(github.event.inputs.image_versions)}}

runs-on: ubuntu-latest
permissions:
contents: read
packages: write
setup:
name: Normalize Inputs
runs-on: ubuntu-24.04
outputs:
ruby_versions_json: ${{ steps.normalize.outputs.ruby_versions_json }}
image_versions_json: ${{ steps.normalize.outputs.image_versions_json }}
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v6

- name: Build and Publish Image
uses: ./.github/actions/build-and-publish-image
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby_version: ${{ matrix.RUBY_VERSION }}
image_tag: ${{ matrix.IMAGE_VERSION }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
repository_owner: ${{ github.repository_owner }}
ruby-version: .ruby-version

- id: normalize
env:
RUBY_VERSIONS: ${{ github.event.inputs.ruby_versions }}
IMAGE_VERSIONS: ${{ github.event.inputs.image_versions }}
REPOSITORY: ${{ github.repository }}
run: bin/normalize-publish-inputs

publish:
name: Publish Images
needs: setup
uses: ./.github/workflows/publish-images-reusable.yaml
with:
ruby_versions_json: ${{ needs.setup.outputs.ruby_versions_json }}
image_versions_json: ${{ needs.setup.outputs.image_versions_json }}
repository_owner: ${{ github.repository_owner }}
secrets:
gh_token: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: write
68 changes: 68 additions & 0 deletions bin/normalize-publish-inputs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "optparse"
require_relative "../lib/commands/publish_input_normalizer"

options = {
ruby_versions: ENV["RUBY_VERSIONS"],
image_versions: ENV["IMAGE_VERSIONS"] || "",
repository: ENV["REPOSITORY"] || ENV["GITHUB_REPOSITORY"]
}

parser = OptionParser.new do |opts|
opts.banner = "Usage: ruby bin/normalize-publish-inputs [--ruby-versions LIST] [--image-versions LIST] [--repository OWNER/REPO]"

opts.on("--ruby-versions VALUE", "Comma/newline-separated Ruby versions") do |value|
options[:ruby_versions] = value
end

opts.on("--image-versions VALUE", "Optional comma/newline-separated image versions") do |value|
options[:image_versions] = value
end

opts.on("--repository VALUE", "Repository in OWNER/REPO format (defaults to GITHUB_REPOSITORY)") do |value|
options[:repository] = value
end

opts.on("-h", "--help", "Show this help") do
puts opts
exit 0
end
end

begin
parser.parse!

unless options[:ruby_versions] && !options[:ruby_versions].strip.empty?
warn "--ruby-versions is required"
warn parser
exit 1
end

result = Commands::PublishInputNormalizer.call(
ruby_versions_input: options[:ruby_versions],
image_versions_input: options[:image_versions],
repository: options[:repository]
)

if ENV["GITHUB_OUTPUT"]
File.open(ENV["GITHUB_OUTPUT"], "a") do |f|
f.puts "ruby_versions_json=#{result[:ruby_versions_json]}"
f.puts "image_versions_json=#{result[:image_versions_json]}"
end
else
puts JSON.pretty_generate({
ruby_versions_json: result[:ruby_versions_json],
image_versions_json: result[:image_versions_json]
})
end
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
warn e.message
warn parser
exit 1
rescue Commands::PublishInputNormalizer::Error => e
warn e.message
exit 1
end
Loading