Engineering Elixir Applications - Github error when creating PR for the "Updating Your CI/CD Workflow to Include a PostgreSQL Database" section (page 149)

Ellie Fairholm @elliefairholm and Josep Giralt D’Lacoste @Gilacost,

After pushing the new branch in order to create the PR at the end of the “Updating Your CI/CD Workflow to Include a PostgreSQL Database” section, I’m getting the following error in Github during the build:

In the “Run docker/build-push-action@v5” step, the following error occurs: “Error: buildx failed with: ERROR: failed to solve: hexpm/elixir:1.16.0-erlang–debian-bullseye-20231009-slim: failed to resolve source metadata for docker.io/hexpm/elixir:1.16.0-erlang--debian-bullseye-20231009-slim: docker.io/hexpm/elixir:1.16.0-erlang--debian-bullseye-20231009-slim: not found”

Anyone else have any tips on how to get around this?

HI @SlowburnAZ. I think you might have an issue that is not related to the update in the CI workflow. It looks like you are trying to build an image that does not exist: docker.io/hexpm/elixir:1.16.0-erlang--debian-bullseye-20231009-slim. If you look at the registry URL you will see that the Erlang version is not included. Could you please share your .tool-versions, .githubt/workflows/ci_cd.yaml and scripts/versions.sh files?

Thanks, @Gilacost

Yeah, it seems I’m missing something somewhere, or there’s a typo I’m not catching.

Here’s my .tool-versions file:

github-cli 2.42.1
terraform 1.7.1
awscli 2.15.15
elixir 1.16.0-otp-26
erlang 26.2.1
postgres 15.2
packer 1.9.0
age 1.1.1
jq 1.7
sops 3.8.1

Here’s the .github/workflows/ci_cd.yaml file:

name: CI/CD Elixir

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  ci:
    runs-on: ubuntu-latest
    name: Compile with mix test, format, dialyzer & unused deps check
    env:
      MIX_ENV: test
    services:
      postgres:
        image: postgres:15.2
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4

      - name: Setup Elixir
        uses: erlef/setup-beam@v1.17.3
        with:
          version-file: .tool-versions
          version-type: strict
      
      - name: Cache deps directory
        uses: actions/cache@v4
        id: cache-deps
        with:
          path: |
            deps
            _build
          key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ runner.os }}-mix-
      
      - name: Get dependencies
        if: steps.cache-deps.outputs.cache-hit != 'true'
        run: mix deps.get
      
      - name: Check unused dependencies
        run: mix deps.unlock --check-unused

      - run: mix compile --warnings-as-errors

      - name: Check code is formatted
        run: mix format --check-formatted

      - name: Run tests
        run: mix test --max-failures 1 --trace --warnings-as-errors

      - name: Cache plt files
        uses: actions/cache@v4
        env:
          EX_OTP_VERSIONS: ${{ steps.setup-beam.outputs.elixir-version }}
          KEY_BASE: plt-${{ runner.os }}-${{ env.EX_OTP_VERSIONS }}
        with:
          path: |
            priv/plts
          key: |
            ${{ env.KEY_BASE }}-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ env.KEY_BASE }}-

      - name: Dialyzer static analysis
        run: mix dialyzer --format github

  build-push:
    runs-on: ubuntu-latest
    needs: ci
    name: Build Docker image & push to ghcr.io
    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GH_PAT }}
      
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/{myusername}/kanban
          tags: |
            type=raw,value=latest,enable={{is_default_branch}}
            type=ref,event=pr
            type=sha,format=short
      
      - name: Parse versions from .tool-versions
        id: parse-asdf
        run: ./scripts/versions.sh
      
      - uses: docker/setup-buildx-action@v3

      - name: Setup QEMU
        uses: docker/setup-qemu-action@v3

      - uses: docker/build-push-action@v5
        with:
          context: .
          cache-from: type=registry,ref=ghcr.io/{myusername}/kanban:cache
          cache-to: type=registry,ref=ghcr.io/{myusername}/kanban:cache,mode=max
          tags: ${{ steps.meta.outputs.tags }}
          platforms: linux/amd64, linux/arm64
          labels: ${{ steps.meta.outputs.labels }}
          push: true
          build-args: |
            ELIXIR_VERSION=${{ env.ELIXIR_VERSION }}
            OTP_VERSION=${{ env.ERLANG_VERSION }}

and finally, the scripts/versions.sh file:

#!/bin/bash
set -x

# read the version information from the ./tool-versions file
version_output=$(cat ./.tool-versions)

# extract the Elixir version
ELIXIR_VERSION=$(echo "$version_output" | grep 'elixir' | cut -d' ' -f2 | cut -d'-' -f1)

# extract Erlang version
ERLANG_VERSION=$(echo "$version_output" | grep 'erlang' | cur -d' ' -f2)

# add the variables to the `GITHUB_ENV` (env used by the action's runner)
{
    echo "ELIXIR_VERSION=${ELIXIR_VERSION}";
    echo "ERLANG_VERSION=${ERLANG_VERSION}";
} >> "$GITHUB_ENV"

Also, here’s the Dockerfile that the mix phx.gen.release --docker command produced:

# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
# instead of Alpine to avoid DNS resolution issues in production.
#
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
# https://hub.docker.com/_/ubuntu?tab=tags
#
# This file is based on these images:
#
#   - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
#   - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20231009-slim - for the release image
#   - https://pkgs.org/ - resource for finding needed packages
#   - Ex: hexpm/elixir:1.16.0-erlang-26.2.1-debian-bullseye-20231009-slim
#
ARG ELIXIR_VERSION=1.16.0
ARG OTP_VERSION=26.2.1
ARG DEBIAN_VERSION=bullseye-20231009-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"

FROM ${BUILDER_IMAGE} as builder

ENV ERL_FLAGS="+JPperf true"

# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \
  && apt-get clean && rm -f /var/lib/apt/lists/*_*

# prepare build dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force && \
  mix local.rebar --force

# set build ENV
ENV MIX_ENV="prod"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config

# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile

COPY priv priv

COPY lib lib

COPY assets assets

# compile assets
RUN mix assets.deploy

# Compile the release
RUN mix compile

# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/

COPY rel rel
RUN mix release

# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE}

RUN apt-get update -y && \
  apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
  && apt-get clean && rm -f /var/lib/apt/lists/*_*

# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen

ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

WORKDIR "/app"
RUN chown nobody /app

# set runner ENV
ENV MIX_ENV="prod"

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/kanban ./

USER nobody

# If using an environment that doesn't automatically reap zombie processes, it is
# advised to add an init process such as tini via `apt-get install`
# above and adding an entrypoint. See https://github.com/krallin/tini for details
# ENTRYPOINT ["/tini", "--"]

CMD ["/app/bin/server"]

It seems the “OTP_VERSION” argument is defined and correct. Not sure why it’s not being included in the registry URL… ??

Thank you for sharing the files! You have a typo in your versions.sh script in the bit that is trying to extract the Erlang version: it says “cur” rather than “cut”. Your versions.sh should look like this:

#!/bin/bash
set -x

# read the version information from the ./tool-versions file
version_output=$(cat ./.tool-versions)

# extract the Elixir version
ELIXIR_VERSION=$(echo "$version_output" | grep 'elixir' | cut -d' ' -f2 | cut -d'-' -f1)

# extract Erlang version
ERLANG_VERSION=$(echo "$version_output" | grep 'erlang' | cut -d' ' -f2)

# add the variables to the `GITHUB_ENV` (env used by the action's runner)
{
    echo "ELIXIR_VERSION=${ELIXIR_VERSION}";
    echo "ERLANG_VERSION=${ERLANG_VERSION}";
} >> "$GITHUB_ENV"

To validate that this is working as expected, execute your ./scripts/versions.sh script and check that it echoes the right ERLANG and ELIXIR versions.

Please let me know how it goes.

Oh geez, figures. Thank you! Yes, that shows the correct versions now.

1 Like