From 755c00c958e861990c3c70961ed33863e546fd60 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sun, 23 Aug 2026 11:06:26 -0700 Subject: [PATCH 1/3] Fix both CI install steps Every job on the Unit Tests workflow currently fails before running a single spec. Two independent causes, one per job. docker-rspec: the ubuntu-24.04 runner image now preinstalls a docker-ce newer than the 26.x/27.x versions pinned in the matrix, so installing the pin is a downgrade and apt refuses it: The following packages will be DOWNGRADED: docker-ce E: Packages were downgraded and -y was used without --allow-downgrades. Pass --allow-downgrades. The existing "Could not install" diagnostic was unreachable, since the step runs under `bash -e` and exits at the failing apt-get before the `if` is evaluated; check that madison actually matched a version instead, so an unmatched pin reports the available versions. podman-rspec: script/install_podman.sh pulled podman from the openSUSE Build Service repo devel:kubic:libcontainers:stable. That project was retired and Release.key now serves an HTML "Resource is no longer available!" page, so apt-key fails: gpg: no valid OpenPGP data found. Ubuntu ships podman in universe (4.9.3 on 24.04), so drop the third-party repo and install from Ubuntu directly. Also replace apt-key with a signed-by keyring for the Docker repo. apt-key is deprecated, emits a warning on every run, and is slated for removal, which would break this workflow a second time. --- .github/workflows/unit_test.yml | 19 +++++++++++++------ script/install_podman.sh | 14 +++++++++----- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 4fb73b76..030ed4ab 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -42,19 +42,26 @@ jobs: set -x sudo apt-get remove -y docker docker-engine docker.io containerd runc ||: sudo apt-get update -y - sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + sudo apt-get install -y apt-transport-https ca-certificates curl gnupg + # apt-key is deprecated and slated for removal; use a keyring instead. + sudo install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo tee /etc/apt/keyrings/docker.asc > /dev/null + sudo chmod a+r /etc/apt/keyrings/docker.asc + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -y sudo apt-cache gencaches - sudo apt-get install -y docker-ce=$( apt-cache madison docker-ce | grep -e $DOCKER_VERSION | cut -f 2 -d '|' | head -1 | sed 's/\s//g' ) - if [ $? -ne 0 ]; then - echo "Error: Could not install ${DOCKER_VERSION}" + version=$( apt-cache madison docker-ce | grep -e "$DOCKER_VERSION" | cut -f 2 -d '|' | head -1 | sed 's/\s//g' ) + if [ -z "$version" ]; then + echo "Error: Could not find a docker-ce package matching ${DOCKER_VERSION}" echo "Available docker versions:" apt-cache madison docker-ce exit 1 fi + # The runner image preinstalls a newer docker-ce than the versions + # pinned in the matrix, so installing the pin is a downgrade. + sudo apt-get install -y --allow-downgrades docker-ce="$version" sudo systemctl start docker + docker version - name: spec tests run: bundle exec rake diff --git a/script/install_podman.sh b/script/install_podman.sh index f5d48789..83cd055e 100755 --- a/script/install_podman.sh +++ b/script/install_podman.sh @@ -1,12 +1,16 @@ #!/bin/sh set -ex -. /etc/os-release - -curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key | sudo apt-key add - - -echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" > /etc/apt/sources.list.d/podman.list +# Ubuntu ships podman in the universe repository (4.9.3 on 24.04), so no +# third-party repository is required. +# +# This previously pulled podman from the openSUSE Build Service repo +# devel:kubic:libcontainers:stable. That project was retired and the repo now +# serves an HTML "Resource is no longer available!" page in place of +# Release.key, so `apt-key add` failed with "no valid OpenPGP data found". apt-get update apt-get install -y podman + +podman --version From 7a1bd31a6e636976ab402ab93c919c39003c389e Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sun, 23 Aug 2026 11:16:12 -0700 Subject: [PATCH 2/3] Test through Ruby 4.0, pin actions by SHA, add dependabot With the install steps fixed the specs actually run again, which exposed the remaining failures and let the matrix be extended. Ruby 3.x spec failure: `its(:resource)` fails on every Ruby 3.x job with "private method 'resource' called". #resource is deliberately private and rspec-its dispatches with public_send on Ruby 3.0+. Call it with send from a plain example instead. This was the only spec failure; all twelve Ruby 3.x jobs failed on it and every 2.x job passed. Ruby 3.4 / 4.0 support: base64 stopped being a default gem in Ruby 3.4, so `require 'base64'` in lib/docker.rb raises LoadError under Bundler. The gem is simply unusable on 3.4+ right now. Declare base64 as a runtime dependency. On older rubies it resolves to the default gem, so nothing changes there. Add 3.4 and 4.0 to both job matrices. Supply chain: pin actions/checkout and ruby/setup-ruby to commit SHAs rather than mutable tags, updated to their latest releases (checkout v4 -> v7.0.1, setup-ruby v1 -> v1.321.0). The checkout bump also clears the Node 20 deprecation warning. Add .github/dependabot.yml so the pinned SHAs still get updated, with a 7 day cooldown so a compromised release is not picked up the moment it is published. --- .github/dependabot.yml | 16 ++++++++++++++++ .github/workflows/unit_test.yml | 12 ++++++++---- docker-api.gemspec | 3 +++ spec/docker/connection_spec.rb | 6 +++++- 4 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3e141601 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + # Wait a week before proposing a newly published action version, so a + # compromised release has time to be caught and pulled. + cooldown: + default-days: 7 + commit-message: + prefix: "ci" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 030ed4ab..70963d8e 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -15,6 +15,8 @@ jobs: strategy: matrix: ruby: + - '4.0' + - 3.4 - 3.3 - 3.2 - 3.1 @@ -28,8 +30,8 @@ jobs: - ':27.' fail-fast: false steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true @@ -71,6 +73,8 @@ jobs: strategy: matrix: ruby: + - '4.0' + - 3.4 - 3.3 - 3.2 - 3.1 @@ -81,8 +85,8 @@ jobs: - 2.4 fail-fast: false steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true diff --git a/docker-api.gemspec b/docker-api.gemspec index 1a131c76..86694c7b 100644 --- a/docker-api.gemspec +++ b/docker-api.gemspec @@ -14,6 +14,9 @@ Gem::Specification.new do |gem| gem.version = Docker::VERSION gem.add_dependency 'excon', '>= 0.64.0' gem.add_dependency 'multi_json' + # base64 stopped being a default gem in Ruby 3.4, so `require 'base64'` in + # lib/docker.rb raises LoadError under Bundler unless it is declared. + gem.add_dependency 'base64' gem.add_development_dependency 'rake' gem.add_development_dependency 'rspec', '~> 3.0' gem.add_development_dependency 'rspec-its' diff --git a/spec/docker/connection_spec.rb b/spec/docker/connection_spec.rb index 86291298..43bc7260 100644 --- a/spec/docker/connection_spec.rb +++ b/spec/docker/connection_spec.rb @@ -66,7 +66,11 @@ end describe '#resource' do - its(:resource) { should be_a Excon::Connection } + # #resource is private, so it cannot go through `its`, which uses + # public_send on Ruby 3.0+. + it 'is an Excon::Connection' do + expect(subject.send(:resource)).to be_a Excon::Connection + end end describe '#request' do From 729c0c64d35b8502ef9fb4bdbe87a3ba140c7906 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sun, 23 Aug 2026 11:31:51 -0700 Subject: [PATCH 3/3] Fix race in Container#stop spec docker-rspec (2.5, :26.) failed on this while the other 29 jobs passed, including Ruby 2.5 against Docker 27: 1) Docker::Container#stop stops the container expected `[...].none?` to be truthy, got false POST /containers/{id}/stop returns once the daemon has signalled the container, which can land just before it leaves the running list, so listing running containers immediately afterwards is a race. Wait on the not-running condition before asserting. Container#wait is already the synchronisation primitive used elsewhere in this file, and POST /containers/{id}/wait defaults to exactly the not-running condition the assertion checks. --- spec/docker/container_spec.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/docker/container_spec.rb b/spec/docker/container_spec.rb index d5c8a34f..927556bd 100644 --- a/spec/docker/container_spec.rb +++ b/spec/docker/container_spec.rb @@ -503,7 +503,14 @@ described_class.create('Cmd' => %w[true], 'Image' => 'debian:stable') } - before { subject.tap(&:start).stop('timeout' => '10') } + # POST /containers/{id}/stop returns once the daemon has signalled the + # container, which can land just before it leaves the running list. Wait on + # the not-running condition so the assertion below is not racing that + # transition. + before do + subject.tap(&:start).stop('timeout' => '10') + subject.wait(10) + end after { subject.remove } it 'stops the container' do