From 9590cc46c6b29d74a873c02bda570701a8412c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Wed, 22 Apr 2026 04:12:06 +0000 Subject: [PATCH 1/7] Improve the image builds by building and pushing architecture-specific images in parallel Then creating and pushing a multi-arch manifest in a separate step. This allows us to build for amd64 and arm64 simultaneously, reducing overall build time. --- .../build-and-publish-image/action.yml | 48 +++++++++++++++---- .../workflows/publish-new-image-version.yaml | 36 ++++++++++++-- .../workflows/publish-new-ruby-versions.yaml | 36 ++++++++++++-- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/.github/actions/build-and-publish-image/action.yml b/.github/actions/build-and-publish-image/action.yml index 3ae2cf4..75a2410 100644 --- a/.github/actions/build-and-publish-image/action.yml +++ b/.github/actions/build-and-publish-image/action.yml @@ -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" @@ -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 @@ -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 diff --git a/.github/workflows/publish-new-image-version.yaml b/.github/workflows/publish-new-image-version.yaml index a7d34f4..bd79eca 100644 --- a/.github/workflows/publish-new-image-version.yaml +++ b/.github/workflows/publish-new-image-version.yaml @@ -5,7 +5,7 @@ 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: @@ -20,16 +20,44 @@ jobs: fail-fast: false matrix: RUBY_VERSION: ${{ fromJSON(needs.setup.outputs.matrix) }} - runs-on: ubuntu-latest + 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: Checkout (GitHub) - uses: actions/checkout@v6 - 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: ${{ github.ref_name }} + gh_token: ${{ secrets.GITHUB_TOKEN }} + repository_owner: ${{ github.repository_owner }} + + publish-manifests: + name: Publish Multi-Arch Manifests + needs: [setup, build] + strategy: + fail-fast: false + matrix: + RUBY_VERSION: ${{ fromJSON(needs.setup.outputs.matrix) }} + 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: ${{ github.ref_name }} gh_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-new-ruby-versions.yaml b/.github/workflows/publish-new-ruby-versions.yaml index 94a3cc0..55fecf3 100644 --- a/.github/workflows/publish-new-ruby-versions.yaml +++ b/.github/workflows/publish-new-ruby-versions.yaml @@ -21,19 +21,47 @@ jobs: matrix: RUBY_VERSION: ${{ fromJSON(github.event.inputs.ruby_versions)}} IMAGE_VERSION: ${{ fromJSON(github.event.inputs.image_versions)}} + 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: ubuntu-latest + runs-on: ${{ matrix.TARGET.RUNNER }} 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 }} + build_platform: ${{ matrix.TARGET.BUILD_PLATFORM }} + platform_suffix: ${{ matrix.TARGET.PLATFORM_SUFFIX }} image_tag: ${{ matrix.IMAGE_VERSION }} gh_token: ${{ secrets.GITHUB_TOKEN }} repository_owner: ${{ github.repository_owner }} + + publish-manifests: + name: Publish Multi-Arch Manifests + needs: build + strategy: + fail-fast: false + matrix: + RUBY_VERSION: ${{ fromJSON(github.event.inputs.ruby_versions)}} + IMAGE_VERSION: ${{ fromJSON(github.event.inputs.image_versions)}} + 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.GITHUB_TOKEN }} + repository_owner: ${{ github.repository_owner }} From 7a679f9190d47a978f5c137c7a5c24d019faae96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Wed, 22 Apr 2026 04:19:40 +0000 Subject: [PATCH 2/7] Remove duplicated code from the publish image actions --- .../workflows/publish-images-reusable.yaml | 77 +++++++++++++++++++ .../workflows/publish-new-image-version.yaml | 54 +++---------- .../workflows/publish-new-ruby-versions.yaml | 58 +++----------- 3 files changed, 95 insertions(+), 94 deletions(-) create mode 100644 .github/workflows/publish-images-reusable.yaml diff --git a/.github/workflows/publish-images-reusable.yaml b/.github/workflows/publish-images-reusable.yaml new file mode 100644 index 0000000..4dee05f --- /dev/null +++ b/.github/workflows/publish-images-reusable.yaml @@ -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 }} diff --git a/.github/workflows/publish-new-image-version.yaml b/.github/workflows/publish-new-image-version.yaml index bd79eca..f0c7775 100644 --- a/.github/workflows/publish-new-image-version.yaml +++ b/.github/workflows/publish-new-image-version.yaml @@ -13,52 +13,16 @@ jobs: - 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) }} - 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 }} + 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: 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: ${{ github.ref_name }} - gh_token: ${{ secrets.GITHUB_TOKEN }} - repository_owner: ${{ github.repository_owner }} - - publish-manifests: - name: Publish Multi-Arch Manifests - needs: [setup, build] - strategy: - fail-fast: false - matrix: - RUBY_VERSION: ${{ fromJSON(needs.setup.outputs.matrix) }} - 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: ${{ github.ref_name }} - gh_token: ${{ secrets.GITHUB_TOKEN }} - repository_owner: ${{ github.repository_owner }} diff --git a/.github/workflows/publish-new-ruby-versions.yaml b/.github/workflows/publish-new-ruby-versions.yaml index 55fecf3..8c6712a 100644 --- a/.github/workflows/publish-new-ruby-versions.yaml +++ b/.github/workflows/publish-new-ruby-versions.yaml @@ -13,55 +13,15 @@ on: description: List of image versions to build. Should be an array ["ruby-1.1.0"] 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)}} - 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.GITHUB_TOKEN }} - repository_owner: ${{ github.repository_owner }} - - publish-manifests: - name: Publish Multi-Arch Manifests - needs: build - strategy: - fail-fast: false - matrix: - RUBY_VERSION: ${{ fromJSON(github.event.inputs.ruby_versions)}} - IMAGE_VERSION: ${{ fromJSON(github.event.inputs.image_versions)}} - runs-on: ubuntu-24.04 + publish: + name: Publish Images + uses: ./.github/workflows/publish-images-reusable.yaml + with: + ruby_versions_json: ${{ github.event.inputs.ruby_versions }} + image_versions_json: ${{ github.event.inputs.image_versions }} + repository_owner: ${{ github.repository_owner }} + secrets: + gh_token: ${{ secrets.GITHUB_TOKEN }} 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.GITHUB_TOKEN }} - repository_owner: ${{ github.repository_owner }} From c06d32e1c49d00aa785ab4d3fa9b4603ea3a43df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Wed, 22 Apr 2026 04:28:35 +0000 Subject: [PATCH 3/7] Make easier to publish new versions by not having to type json --- .../workflows/publish-new-ruby-versions.yaml | 28 ++++- bin/normalize-publish-inputs | 68 ++++++++++++ lib/commands/publish_input_normalizer.rb | 103 ++++++++++++++++++ .../commands/publish_input_normalizer_test.rb | 61 +++++++++++ 4 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 bin/normalize-publish-inputs create mode 100644 lib/commands/publish_input_normalizer.rb create mode 100644 test/commands/publish_input_normalizer_test.rb diff --git a/.github/workflows/publish-new-ruby-versions.yaml b/.github/workflows/publish-new-ruby-versions.yaml index 8c6712a..a351bb2 100644 --- a/.github/workflows/publish-new-ruby-versions.yaml +++ b/.github/workflows/publish-new-ruby-versions.yaml @@ -6,19 +6,37 @@ 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: + 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 + + - id: normalize + run: | + ruby bin/normalize-publish-inputs \ + --ruby-versions '${{ github.event.inputs.ruby_versions }}' \ + --image-versions '${{ github.event.inputs.image_versions }}' \ + --repository '${{ github.repository }}' + publish: name: Publish Images + needs: setup uses: ./.github/workflows/publish-images-reusable.yaml with: - ruby_versions_json: ${{ github.event.inputs.ruby_versions }} - image_versions_json: ${{ github.event.inputs.image_versions }} + 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 }} diff --git a/bin/normalize-publish-inputs b/bin/normalize-publish-inputs new file mode 100644 index 0000000..119c9ee --- /dev/null +++ b/bin/normalize-publish-inputs @@ -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: nil, + image_versions: "", + 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 diff --git a/lib/commands/publish_input_normalizer.rb b/lib/commands/publish_input_normalizer.rb new file mode 100644 index 0000000..6a00495 --- /dev/null +++ b/lib/commands/publish_input_normalizer.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require "json" +require "open3" +require "rubygems" + +module Commands + class PublishInputNormalizer + class Error < StandardError; end + + class << self + def call(ruby_versions_input:, image_versions_input:, repository:, latest_tag_fetcher: nil) + new( + ruby_versions_input: ruby_versions_input, + image_versions_input: image_versions_input, + repository: repository, + latest_tag_fetcher: latest_tag_fetcher + ).call + end + end + + attr_reader :ruby_versions_input, :image_versions_input, :repository, :latest_tag_fetcher + + def initialize(ruby_versions_input:, image_versions_input:, repository:, latest_tag_fetcher: nil) + @ruby_versions_input = ruby_versions_input.to_s + @image_versions_input = image_versions_input.to_s + @repository = repository.to_s + @latest_tag_fetcher = latest_tag_fetcher || method(:fetch_latest_image_tag) + end + + def call + ruby_versions = normalize_list(ruby_versions_input) + raise Error, "Ruby versions input is empty" if ruby_versions.empty? + + images_source = image_versions_input + if blank_list?(images_source) + images_source = latest_tag_fetcher.call(repository) + end + + image_versions = normalize_image_versions(normalize_list(images_source)) + raise Error, "Image versions input is empty" if image_versions.empty? + + { + ruby_versions: ruby_versions, + image_versions: image_versions, + ruby_versions_json: JSON.generate(ruby_versions), + image_versions_json: JSON.generate(image_versions) + } + end + + private + + def blank_list?(value) + value.to_s.gsub(/[\s,]/, "").empty? + end + + def normalize_list(value) + value + .to_s + .split(/[\n,]/) + .map(&:strip) + .reject(&:empty?) + end + + def normalize_image_versions(versions) + versions.map { |version| version.start_with?("ruby-") ? version : "ruby-#{version}" } + end + + def fetch_latest_image_tag(repo) + raise Error, "Repository is required to discover latest image tag" if repo.empty? + + stdout, status = Open3.capture2( + "git", + "ls-remote", + "--tags", + "--refs", + "https://github.com/#{repo}.git", + "refs/tags/ruby-*" + ) + + unless status.success? + raise Error, "Unable to resolve latest ruby-* image tag" + end + + tags = stdout.lines.map do |line| + ref = line.split[1] + ref&.sub("refs/tags/", "") + end.compact + + latest = tags.max_by { |tag| version_for(tag) } + raise Error, "Unable to resolve latest ruby-* image tag" unless latest + + latest + end + + def version_for(tag) + raw = tag.sub(/^ruby-/, "") + Gem::Version.new(raw) + rescue ArgumentError + Gem::Version.new("0") + end + end +end diff --git a/test/commands/publish_input_normalizer_test.rb b/test/commands/publish_input_normalizer_test.rb new file mode 100644 index 0000000..c059437 --- /dev/null +++ b/test/commands/publish_input_normalizer_test.rb @@ -0,0 +1,61 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "minitest/autorun" +require_relative "../../lib/commands/publish_input_normalizer" + +class Commands::PublishInputNormalizerTest < Minitest::Test + def test_normalizes_comma_and_newline_lists + result = Commands::PublishInputNormalizer.call( + ruby_versions_input: "3.3.1, 3.2.4\n3.1.9", + image_versions_input: "1.1.0, ruby-1.2.0", + repository: "rails/devcontainer" + ) + + assert_equal ["3.3.1", "3.2.4", "3.1.9"], result[:ruby_versions] + assert_equal ["ruby-1.1.0", "ruby-1.2.0"], result[:image_versions] + assert_equal '["3.3.1","3.2.4","3.1.9"]', result[:ruby_versions_json] + assert_equal '["ruby-1.1.0","ruby-1.2.0"]', result[:image_versions_json] + end + + def test_uses_latest_image_tag_when_image_versions_blank + fetcher = ->(_repo) { "ruby-2.0.0" } + + result = Commands::PublishInputNormalizer.call( + ruby_versions_input: "3.3.1", + image_versions_input: " , \n", + repository: "rails/devcontainer", + latest_tag_fetcher: fetcher + ) + + assert_equal ["ruby-2.0.0"], result[:image_versions] + assert_equal '["ruby-2.0.0"]', result[:image_versions_json] + end + + def test_raises_for_empty_ruby_versions + error = assert_raises(Commands::PublishInputNormalizer::Error) do + Commands::PublishInputNormalizer.call( + ruby_versions_input: " , \n", + image_versions_input: "1.1.0", + repository: "rails/devcontainer" + ) + end + + assert_match(/Ruby versions input is empty/, error.message) + end + + def test_raises_when_latest_tag_is_missing + fetcher = ->(_repo) { nil } + + error = assert_raises(Commands::PublishInputNormalizer::Error) do + Commands::PublishInputNormalizer.call( + ruby_versions_input: "3.3.1", + image_versions_input: "", + repository: "rails/devcontainer", + latest_tag_fetcher: fetcher + ) + end + + assert_match(/Image versions input is empty/, error.message) + end +end From c6f5c2c030e1d8831d6361379efb95bca03f9d9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:01:08 +0000 Subject: [PATCH 4/7] Use repo Ruby and env vars in publish-new-ruby-versions workflow Agent-Logs-Url: https://github.com/rails/devcontainer/sessions/ef2fb079-610d-4b64-b61b-851dd2c2d34f Co-authored-by: rafaelfranca <47848+rafaelfranca@users.noreply.github.com> --- .github/workflows/publish-new-ruby-versions.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-new-ruby-versions.yaml b/.github/workflows/publish-new-ruby-versions.yaml index a351bb2..53a2f3c 100644 --- a/.github/workflows/publish-new-ruby-versions.yaml +++ b/.github/workflows/publish-new-ruby-versions.yaml @@ -23,12 +23,21 @@ jobs: - name: Checkout (GitHub) uses: actions/checkout@v6 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + 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: | ruby bin/normalize-publish-inputs \ - --ruby-versions '${{ github.event.inputs.ruby_versions }}' \ - --image-versions '${{ github.event.inputs.image_versions }}' \ - --repository '${{ github.repository }}' + --ruby-versions "$RUBY_VERSIONS" \ + --image-versions "$IMAGE_VERSIONS" \ + --repository "$REPOSITORY" publish: name: Publish Images From 764953ee3c08f0116b947dd350b8b0c972606b8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:47:02 +0000 Subject: [PATCH 5/7] Fallback to env vars in normalize script and remove workflow args Agent-Logs-Url: https://github.com/rails/devcontainer/sessions/23db5954-a602-4ea7-94c3-bd7a0be7c1f5 Co-authored-by: rafaelfranca <47848+rafaelfranca@users.noreply.github.com> --- .github/workflows/publish-new-ruby-versions.yaml | 6 +----- bin/normalize-publish-inputs | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish-new-ruby-versions.yaml b/.github/workflows/publish-new-ruby-versions.yaml index 53a2f3c..70136c5 100644 --- a/.github/workflows/publish-new-ruby-versions.yaml +++ b/.github/workflows/publish-new-ruby-versions.yaml @@ -33,11 +33,7 @@ jobs: RUBY_VERSIONS: ${{ github.event.inputs.ruby_versions }} IMAGE_VERSIONS: ${{ github.event.inputs.image_versions }} REPOSITORY: ${{ github.repository }} - run: | - ruby bin/normalize-publish-inputs \ - --ruby-versions "$RUBY_VERSIONS" \ - --image-versions "$IMAGE_VERSIONS" \ - --repository "$REPOSITORY" + run: ruby bin/normalize-publish-inputs publish: name: Publish Images diff --git a/bin/normalize-publish-inputs b/bin/normalize-publish-inputs index 119c9ee..03e0d47 100644 --- a/bin/normalize-publish-inputs +++ b/bin/normalize-publish-inputs @@ -6,13 +6,13 @@ require "optparse" require_relative "../lib/commands/publish_input_normalizer" options = { - ruby_versions: nil, - image_versions: "", - repository: ENV["GITHUB_REPOSITORY"] + 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.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 From 143747a87ab9e84d40b3984d5e315ae6a714f48a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:48:01 +0000 Subject: [PATCH 6/7] Add tests for env fallback in normalize publish script Agent-Logs-Url: https://github.com/rails/devcontainer/sessions/23db5954-a602-4ea7-94c3-bd7a0be7c1f5 Co-authored-by: rafaelfranca <47848+rafaelfranca@users.noreply.github.com> --- .../normalize_publish_inputs_script_test.rb | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/commands/normalize_publish_inputs_script_test.rb diff --git a/test/commands/normalize_publish_inputs_script_test.rb b/test/commands/normalize_publish_inputs_script_test.rb new file mode 100644 index 0000000..53d5b2c --- /dev/null +++ b/test/commands/normalize_publish_inputs_script_test.rb @@ -0,0 +1,78 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "minitest/autorun" +require "open3" +require "rbconfig" +require "tempfile" + +class NormalizePublishInputsScriptTest < Minitest::Test + def test_reads_values_from_env_when_args_are_omitted + output = Tempfile.new("github-output") + + status = run_script( + { + "RUBY_VERSIONS" => "3.3.1,3.2.4", + "IMAGE_VERSIONS" => "ruby-1.1.0", + "REPOSITORY" => "rails/devcontainer", + "GITHUB_OUTPUT" => output.path + } + ) + + assert status.success? + assert_equal '["3.3.1","3.2.4"]', parse_output(output.path).fetch("ruby_versions_json") + ensure + output&.close! + end + + def test_prefers_args_over_env_values + output = Tempfile.new("github-output") + + status = run_script( + { + "RUBY_VERSIONS" => "3.0.0", + "IMAGE_VERSIONS" => "ruby-1.1.0", + "REPOSITORY" => "rails/devcontainer", + "GITHUB_OUTPUT" => output.path + }, + "--ruby-versions", "3.3.1", + "--image-versions", "ruby-1.2.0", + "--repository", "rails/devcontainer" + ) + + assert status.success? + result = parse_output(output.path) + assert_equal '["3.3.1"]', result.fetch("ruby_versions_json") + assert_equal '["ruby-1.2.0"]', result.fetch("image_versions_json") + ensure + output&.close! + end + + private + + def run_script(env, *args) + _stdout, _stderr, status = Open3.capture3( + env, + RbConfig.ruby, + script_path, + *args, + chdir: repo_root + ) + status + end + + def parse_output(path) + File.read(path).lines.to_h do |line| + key, value = line.strip.split("=", 2) + [key, value] + end + end + + def script_path + File.join(repo_root, "bin/normalize-publish-inputs") + end + + def repo_root + File.expand_path("../..", __dir__) + end +end From dd0d2818bcc0b2c94cb8317b2de13063e1a6c5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Thu, 23 Apr 2026 03:09:48 +0000 Subject: [PATCH 7/7] Make the binary executable and run it with the correct Ruby version in the workflow --- .github/workflows/publish-new-ruby-versions.yaml | 2 +- bin/normalize-publish-inputs | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 bin/normalize-publish-inputs diff --git a/.github/workflows/publish-new-ruby-versions.yaml b/.github/workflows/publish-new-ruby-versions.yaml index 70136c5..d5b502f 100644 --- a/.github/workflows/publish-new-ruby-versions.yaml +++ b/.github/workflows/publish-new-ruby-versions.yaml @@ -33,7 +33,7 @@ jobs: RUBY_VERSIONS: ${{ github.event.inputs.ruby_versions }} IMAGE_VERSIONS: ${{ github.event.inputs.image_versions }} REPOSITORY: ${{ github.repository }} - run: ruby bin/normalize-publish-inputs + run: bin/normalize-publish-inputs publish: name: Publish Images diff --git a/bin/normalize-publish-inputs b/bin/normalize-publish-inputs old mode 100644 new mode 100755