Skip to content

Commit 5be7b92

Browse files
authored
Merge pull request #124 from rails/rm-faster-buiolds
Faster image build
2 parents ffac2f6 + dd0d281 commit 5be7b92

8 files changed

Lines changed: 470 additions & 48 deletions

File tree

.github/actions/build-and-publish-image/action.yml

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
name: Build and Publish Image
22
description: Steps for building an image for a specific ruby version
33
inputs:
4+
publish_manifest:
5+
required: false
6+
default: "false"
7+
description: "Set to true to publish a multi-arch manifest instead of building"
48
ruby_version:
59
required: true
610
description: "The version of Ruby to build the image for"
11+
build_platform:
12+
required: false
13+
description: "The single platform to build (linux/amd64 or linux/arm64)"
14+
platform_suffix:
15+
required: false
16+
description: "Platform suffix for arch-specific tags (amd64 or arm64)"
717
image_tag:
818
required: true
919
description: "The tag to use for the image"
@@ -16,18 +26,31 @@ inputs:
1626
runs:
1727
using: "composite"
1828
steps:
29+
- name: Validate required inputs
30+
shell: bash
31+
run: |
32+
if [[ "${{ inputs.publish_manifest }}" != "true" ]]; then
33+
if [[ -z "${{ inputs.build_platform }}" || -z "${{ inputs.platform_suffix }}" ]]; then
34+
echo "build_platform and platform_suffix are required when publish_manifest is false"
35+
exit 1
36+
fi
37+
fi
38+
1939
- name: Checkout (GitHub)
40+
if: ${{ inputs.publish_manifest != 'true' }}
2041
uses: actions/checkout@v6
2142
with:
2243
ref: ${{ inputs.image_tag }}
2344

24-
- name: Set up QEMU for multi-architecture builds
25-
uses: docker/setup-qemu-action@v3
26-
2745
- name: Set up Docker Buildx
46+
if: ${{ inputs.publish_manifest != 'true' }}
2847
uses: docker/setup-buildx-action@v3
2948
with:
30-
platforms: linux/amd64,linux/arm64
49+
platforms: ${{ inputs.build_platform }}
50+
51+
- name: Set up Docker Buildx (manifest)
52+
if: ${{ inputs.publish_manifest == 'true' }}
53+
uses: docker/setup-buildx-action@v3
3154

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

4366
- name: Pre-build Dev Container Image
67+
if: ${{ inputs.publish_manifest != 'true' }}
4468
uses: devcontainers/ci@v0.3
4569
env:
4670
RUBY_VERSION: ${{ inputs.ruby_version }}
4771
BUILDX_NO_DEFAULT_ATTESTATIONS: true
4872
with:
4973
imageName: ghcr.io/rails/devcontainer/images/ruby
50-
imageTag: ${{ env.IMAGE_VERSION }}-${{ inputs.ruby_version }},${{ inputs.ruby_version }}
74+
imageTag: ${{ env.IMAGE_VERSION }}-${{ inputs.ruby_version }}-${{ inputs.platform_suffix }},${{ inputs.ruby_version }}-${{ inputs.platform_suffix }}
75+
cacheFrom: ghcr.io/rails/devcontainer/images/ruby:${{ inputs.ruby_version }}-${{ inputs.platform_suffix }}
5176
subFolder: images/ruby
5277
push: always
53-
platform: linux/amd64,linux/arm64
78+
platform: ${{ inputs.build_platform }}
5479

55-
- name: Checkout (GitHub)
56-
uses: actions/checkout@v6
80+
- name: Create and push multi-arch manifest
81+
if: ${{ inputs.publish_manifest == 'true' }}
82+
shell: bash
83+
run: |
84+
docker buildx imagetools create \
85+
-t ghcr.io/rails/devcontainer/images/ruby:${IMAGE_VERSION}-${{ inputs.ruby_version }} \
86+
-t ghcr.io/rails/devcontainer/images/ruby:${{ inputs.ruby_version }} \
87+
ghcr.io/rails/devcontainer/images/ruby:${IMAGE_VERSION}-${{ inputs.ruby_version }}-amd64 \
88+
ghcr.io/rails/devcontainer/images/ruby:${IMAGE_VERSION}-${{ inputs.ruby_version }}-arm64
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Publish Images Reusable
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
ruby_versions_json:
7+
required: true
8+
type: string
9+
description: JSON array of Ruby versions
10+
image_versions_json:
11+
required: true
12+
type: string
13+
description: JSON array of image tags
14+
repository_owner:
15+
required: true
16+
type: string
17+
description: Repository owner for GHCR login
18+
secrets:
19+
gh_token:
20+
required: true
21+
22+
defaults:
23+
run:
24+
shell: bash
25+
26+
jobs:
27+
build:
28+
name: Build Images
29+
strategy:
30+
fail-fast: false
31+
matrix:
32+
RUBY_VERSION: ${{ fromJSON(inputs.ruby_versions_json) }}
33+
IMAGE_VERSION: ${{ fromJSON(inputs.image_versions_json) }}
34+
TARGET:
35+
- RUNNER: ubuntu-24.04
36+
BUILD_PLATFORM: linux/amd64
37+
PLATFORM_SUFFIX: amd64
38+
- RUNNER: ubuntu-24.04-arm
39+
BUILD_PLATFORM: linux/arm64
40+
PLATFORM_SUFFIX: arm64
41+
42+
runs-on: ${{ matrix.TARGET.RUNNER }}
43+
permissions:
44+
contents: read
45+
packages: write
46+
steps:
47+
- name: Build and Publish Image
48+
uses: ./.github/actions/build-and-publish-image
49+
with:
50+
ruby_version: ${{ matrix.RUBY_VERSION }}
51+
build_platform: ${{ matrix.TARGET.BUILD_PLATFORM }}
52+
platform_suffix: ${{ matrix.TARGET.PLATFORM_SUFFIX }}
53+
image_tag: ${{ matrix.IMAGE_VERSION }}
54+
gh_token: ${{ secrets.gh_token }}
55+
repository_owner: ${{ inputs.repository_owner }}
56+
57+
publish-manifests:
58+
name: Publish Multi-Arch Manifests
59+
needs: build
60+
strategy:
61+
fail-fast: false
62+
matrix:
63+
RUBY_VERSION: ${{ fromJSON(inputs.ruby_versions_json) }}
64+
IMAGE_VERSION: ${{ fromJSON(inputs.image_versions_json) }}
65+
runs-on: ubuntu-24.04
66+
permissions:
67+
contents: read
68+
packages: write
69+
steps:
70+
- name: Publish Multi-Arch Manifest
71+
uses: ./.github/actions/build-and-publish-image
72+
with:
73+
publish_manifest: "true"
74+
ruby_version: ${{ matrix.RUBY_VERSION }}
75+
image_tag: ${{ matrix.IMAGE_VERSION }}
76+
gh_token: ${{ secrets.gh_token }}
77+
repository_owner: ${{ inputs.repository_owner }}

.github/workflows/publish-new-image-version.yaml

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,24 @@ name: Build and Publish Images
55
- ruby-*.*.*
66
jobs:
77
setup:
8-
runs-on: ubuntu-latest
8+
runs-on: ubuntu-24.04
99
outputs:
1010
matrix: ${{ steps.set-matrix.outputs.matrix }}
1111
steps:
1212
- uses: actions/checkout@v6
1313
- id: set-matrix
1414
run: echo "matrix=$(cat .github/ruby-versions.json | jq -c '.')" >> $GITHUB_OUTPUT
1515

16-
build:
17-
name: Build Images
16+
publish:
17+
name: Publish Images
1818
needs: setup
19-
strategy:
20-
fail-fast: false
21-
matrix:
22-
RUBY_VERSION: ${{ fromJSON(needs.setup.outputs.matrix) }}
23-
runs-on: ubuntu-latest
19+
uses: ./.github/workflows/publish-images-reusable.yaml
20+
with:
21+
ruby_versions_json: ${{ needs.setup.outputs.matrix }}
22+
image_versions_json: ${{ format('["{0}"]', github.ref_name) }}
23+
repository_owner: ${{ github.repository_owner }}
24+
secrets:
25+
gh_token: ${{ secrets.GITHUB_TOKEN }}
2426
permissions:
2527
contents: read
2628
packages: write
27-
steps:
28-
- name: Checkout (GitHub)
29-
uses: actions/checkout@v6
30-
- name: Build and Publish Image
31-
uses: ./.github/actions/build-and-publish-image
32-
with:
33-
ruby_version: ${{ matrix.RUBY_VERSION }}
34-
image_tag: ${{ github.ref_name }}
35-
gh_token: ${{ secrets.GITHUB_TOKEN }}
36-
repository_owner: ${{ github.repository_owner }}

.github/workflows/publish-new-ruby-versions.yaml

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,45 @@ on:
66
ruby_versions:
77
type: string
88
required: true
9-
description: List of ruby versions to build. Should be an array ["3.3.1","3.2.4"]
9+
description: Comma or newline-separated Ruby versions (example: 3.3.1, 3.2.4)
1010
image_versions:
1111
type: string
12-
required: true
13-
description: List of image versions to build. Should be an array ["ruby-1.1.0"]
12+
required: false
13+
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.
1414

1515
jobs:
16-
build:
17-
name: Build Images
18-
19-
strategy:
20-
fail-fast: false
21-
matrix:
22-
RUBY_VERSION: ${{ fromJSON(github.event.inputs.ruby_versions)}}
23-
IMAGE_VERSION: ${{ fromJSON(github.event.inputs.image_versions)}}
24-
25-
runs-on: ubuntu-latest
26-
permissions:
27-
contents: read
28-
packages: write
16+
setup:
17+
name: Normalize Inputs
18+
runs-on: ubuntu-24.04
19+
outputs:
20+
ruby_versions_json: ${{ steps.normalize.outputs.ruby_versions_json }}
21+
image_versions_json: ${{ steps.normalize.outputs.image_versions_json }}
2922
steps:
3023
- name: Checkout (GitHub)
3124
uses: actions/checkout@v6
3225

33-
- name: Build and Publish Image
34-
uses: ./.github/actions/build-and-publish-image
26+
- name: Set up Ruby
27+
uses: ruby/setup-ruby@v1
3528
with:
36-
ruby_version: ${{ matrix.RUBY_VERSION }}
37-
image_tag: ${{ matrix.IMAGE_VERSION }}
38-
gh_token: ${{ secrets.GITHUB_TOKEN }}
39-
repository_owner: ${{ github.repository_owner }}
29+
ruby-version: .ruby-version
30+
31+
- id: normalize
32+
env:
33+
RUBY_VERSIONS: ${{ github.event.inputs.ruby_versions }}
34+
IMAGE_VERSIONS: ${{ github.event.inputs.image_versions }}
35+
REPOSITORY: ${{ github.repository }}
36+
run: bin/normalize-publish-inputs
37+
38+
publish:
39+
name: Publish Images
40+
needs: setup
41+
uses: ./.github/workflows/publish-images-reusable.yaml
42+
with:
43+
ruby_versions_json: ${{ needs.setup.outputs.ruby_versions_json }}
44+
image_versions_json: ${{ needs.setup.outputs.image_versions_json }}
45+
repository_owner: ${{ github.repository_owner }}
46+
secrets:
47+
gh_token: ${{ secrets.GITHUB_TOKEN }}
48+
permissions:
49+
contents: read
50+
packages: write

bin/normalize-publish-inputs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "bundler/setup"
5+
require "optparse"
6+
require_relative "../lib/commands/publish_input_normalizer"
7+
8+
options = {
9+
ruby_versions: ENV["RUBY_VERSIONS"],
10+
image_versions: ENV["IMAGE_VERSIONS"] || "",
11+
repository: ENV["REPOSITORY"] || ENV["GITHUB_REPOSITORY"]
12+
}
13+
14+
parser = OptionParser.new do |opts|
15+
opts.banner = "Usage: ruby bin/normalize-publish-inputs [--ruby-versions LIST] [--image-versions LIST] [--repository OWNER/REPO]"
16+
17+
opts.on("--ruby-versions VALUE", "Comma/newline-separated Ruby versions") do |value|
18+
options[:ruby_versions] = value
19+
end
20+
21+
opts.on("--image-versions VALUE", "Optional comma/newline-separated image versions") do |value|
22+
options[:image_versions] = value
23+
end
24+
25+
opts.on("--repository VALUE", "Repository in OWNER/REPO format (defaults to GITHUB_REPOSITORY)") do |value|
26+
options[:repository] = value
27+
end
28+
29+
opts.on("-h", "--help", "Show this help") do
30+
puts opts
31+
exit 0
32+
end
33+
end
34+
35+
begin
36+
parser.parse!
37+
38+
unless options[:ruby_versions] && !options[:ruby_versions].strip.empty?
39+
warn "--ruby-versions is required"
40+
warn parser
41+
exit 1
42+
end
43+
44+
result = Commands::PublishInputNormalizer.call(
45+
ruby_versions_input: options[:ruby_versions],
46+
image_versions_input: options[:image_versions],
47+
repository: options[:repository]
48+
)
49+
50+
if ENV["GITHUB_OUTPUT"]
51+
File.open(ENV["GITHUB_OUTPUT"], "a") do |f|
52+
f.puts "ruby_versions_json=#{result[:ruby_versions_json]}"
53+
f.puts "image_versions_json=#{result[:image_versions_json]}"
54+
end
55+
else
56+
puts JSON.pretty_generate({
57+
ruby_versions_json: result[:ruby_versions_json],
58+
image_versions_json: result[:image_versions_json]
59+
})
60+
end
61+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
62+
warn e.message
63+
warn parser
64+
exit 1
65+
rescue Commands::PublishInputNormalizer::Error => e
66+
warn e.message
67+
exit 1
68+
end

0 commit comments

Comments
 (0)