Compare commits

..
5 Commits
Author SHA1 Message Date
Firehose Bot 7fda0455c1 compile deps in make setup so cold clones stay offline
Some deps download artifacts at compile time: rustler_precompiled
fetches the mdex_native NIF from GitHub during its first compile, and
NimblePublisher pulls mdex_native into both apps. Compiling inside the
deps target keeps that traffic inside setup - the explicit user-run
network step - instead of the first make check/test on a fresh clone.
2026-10-29 18:09:42 +00:00
Firehose Bot 0d344a4f57 make blog slug tests content-independent
Commit 1709cb7 deleted the pinned hello-world post and ten tests
silently rotted with it - invisible until now because the suite could
not run in this clone (incomplete deps, then the broken frontmatter).
The slug, banner, og-meta and tag tests now derive slug and title from
the compiled posts at runtime (newest live post, newest draft), matching
the existing latest_post_with_links pattern, so content churn no longer
breaks them.
2026-10-29 18:05:29 +00:00
Firehose Bot 84539ec40f fix GNU-only \+ in check-pins sed patterns
\+ is a GNU BRE extension; /usr/bin/sed on macOS treats it as a literal
plus, so every Dockerfile ARG pin was reported "not found" and make
check false-failed even though mise.toml and the Dockerfile agree. Use
the POSIX form \{1,\} and note the constraint in the header.
2026-10-29 17:56:41 +00:00
Firehose Bot 97fc024a38 add make setup and network-free deps.check guard
mix deps.get contacts hex.pm even when everything is already installed
(the advisory check), so the everyday loops failed in offline/sandboxed
environments; and the root deps target only installed app's deps, never
blogex's.

make setup (root) installs Hex deps for both apps plus the app's JS
packages, once per clone, explicitly. check, test and test.coverage in
both apps now depend on a local deps.check guard instead: every package
pinned in mix.lock must exist in deps/, else fail loudly pointing at
make setup. AGENTS.md documents the new target.
2026-10-29 17:44:16 +00:00
Firehose Bot 7215cd6db0 fix split closing quote in intention-location-details frontmatter
8fa342e split the description's trailing ", onto its own line, leaving
the frontmatter invalid Elixir; NimblePublisher evals frontmatter at
compile time, so the app could not compile. Also drops the
“/” entities from the description (uncommitted WIP, kept
per plan).
2026-10-29 17:44:16 +00:00
9 changed files with 150 additions and 43 deletions
+6
View File
@@ -70,6 +70,12 @@ This is a Phoenix Liveview monorepo, the blogging library is in blogex, the appl
Main make targets: Main make targets:
- `make setup` - one-time clone setup (and after any mix.exs gains a dep):
fetches Hex deps for both apps, the app's JS packages, and compiles deps
(some deps fetch artifacts at compile time, e.g. the mdex_native NIF).
Needs network - run it yourself; agent sandboxes refuse installs.
check/test/verify never touch the network: they run a local `deps.check`
guard that fails with a pointer here when deps are missing.
- `make check` - cheap static gate for both apps (no DB): app `credo --strict` + - `make check` - cheap static gate for both apps (no DB): app `credo --strict` +
format **check** (`mix format --check-formatted`), blogex compile (warnings format **check** (`mix format --check-formatted`), blogex compile (warnings
fatal) + format **check**, then `scripts/check-pins.sh`. It never rewrites fatal) + format **check**, then `scripts/check-pins.sh`. It never rewrites
+13 -4
View File
@@ -2,7 +2,7 @@
MISE_BIN ?= mise MISE_BIN ?= mise
.PHONY: check precommit verify hooks deps compile test test-elisp format .PHONY: check precommit verify hooks deps setup compile test test-elisp format
# Cheap static gate over both apps, cheapest first. No database, no coverage: # Cheap static gate over both apps, cheapest first. No database, no coverage:
# this is the loop you run freely while working and before every commit. # this is the loop you run freely while working and before every commit.
@@ -47,9 +47,18 @@ hooks:
echo "installed $$hooks_dir/$$name -> $$hook"; \ echo "installed $$hooks_dir/$$name -> $$hook"; \
done done
# Sync dependencies # One-time setup for this clone (and after any mix.exs gains a dependency):
deps: # fetches Hex deps for both apps and the app's JS packages. Needs network, and
@make -C app deps # is deliberately NOT wired into check/test/verify: `mix deps.get` contacts
# hex.pm even when everything is already installed (the advisory check), which
# fails offline/sandboxed. The everyday loops run the per-app `deps.check`
# guard instead and point here when deps are missing.
setup:
@$(MAKE) -C app MISE_BIN=$(MISE_BIN) deps
@$(MAKE) -C blogex MISE_BIN=$(MISE_BIN) deps
# Kept for muscle memory; `setup` is the canonical entry point.
deps: setup
# Compile the project # Compile the project
compile: compile:
+31 -8
View File
@@ -3,12 +3,13 @@
MISE_BIN ?= mise MISE_BIN ?= mise
MISE_EXEC = $(MISE_BIN) exec -- MISE_EXEC = $(MISE_BIN) exec --
.PHONY: check precommit deps assets compile test test.coverage sobelow coverage crap format format.check credo verify .PHONY: check precommit deps deps.check assets compile test test.coverage sobelow coverage crap format format.check credo verify
# Run all static analysis checks (no database required). `format.check` only # Run all static analysis checks (no database, no network required). The
# reports: a gate that rewrites the files it judges can never fail. Fix # deps.check guard fails loudly with a pointer at `make setup` when Hex deps
# formatting with `make format`. # are missing. `format.check` only reports: a gate that rewrites the files it
check: credo format.check # judges can never fail. Fix formatting with `make format`.
check: deps.check credo format.check
# Precommit target for CI/pre-commit hooks # Precommit target for CI/pre-commit hooks
precommit: check compile precommit: check compile
@@ -23,9 +24,31 @@ assets:
@test -f assets/node_modules/.package-lock.json || \ @test -f assets/node_modules/.package-lock.json || \
(echo "==> assets: npm install (first run)"; cd assets && npm install) (echo "==> assets: npm install (first run)"; cd assets && npm install)
# Sync dependencies (Hex packages + JS packages) # Sync dependencies (Hex packages + JS packages) and compile them. The compile
# step is part of setup on purpose: some deps download artifacts at compile
# time - rustler_precompiled fetches the mdex_native NIF from GitHub during
# its first compile, and NimblePublisher pulls mdex_native into this app. Keep
# that traffic in this explicit, user-run step so check/test/verify stay
# offline even on a cold clone. (rustler_precompiled caches artifacts globally
# in ~/Library/Caches/rustler_precompiled, so later test-env compiles find
# them without network.)
deps: assets deps: assets
$(MISE_EXEC) mix deps.get $(MISE_EXEC) mix deps.get
$(MISE_EXEC) mix deps.compile
# Local, network-free guard: every package pinned in mix.lock must be present
# in deps/. check, test and test.coverage depend on this instead of `deps`, so
# the everyday loops never touch the network: `mix deps.get` contacts hex.pm
# even when everything is already installed (the advisory check), which fails
# in offline/sandboxed environments. Fetching is `make setup`'s explicit job,
# run from the repo root.
deps.check:
@test -f mix.lock || { echo "deps.check: no mix.lock - run 'make setup' at the repo root" >&2; exit 1; }
@missing=""; \
for dep in $$(sed -n 's/^[[:space:]]*"\([^"]*\)".*/\1/p' mix.lock); do \
[ -d "deps/$$dep" ] || missing="$$missing $$dep"; \
done; \
test -z "$$missing" || { echo "deps.check: missing:$$missing - run 'make setup' at the repo root" >&2; exit 1; }
# Compile the project # Compile the project
compile: compile:
@@ -33,13 +56,13 @@ compile:
# Run tests (requires PostgreSQL running on localhost:5432) # Run tests (requires PostgreSQL running on localhost:5432)
# Note: If you don't have PostgreSQL, you can skip tests with `make check` # Note: If you don't have PostgreSQL, you can skip tests with `make check`
test: deps compile test: deps.check compile
$(MISE_EXEC) mix test $(MISE_EXEC) mix test
# Run the suite once with coverage exported, so the coverage floor check (and, # Run the suite once with coverage exported, so the coverage floor check (and,
# later, CRAP) reads the export instead of running the suite a second time. # later, CRAP) reads the export instead of running the suite a second time.
# Stale exports are removed first: the floor must never report an older run. # Stale exports are removed first: the floor must never report an older run.
test.coverage: deps compile test.coverage: deps.check compile
rm -f cover/*.coverdata rm -f cover/*.coverdata
$(MISE_EXEC) mix test --cover --export-coverage default $(MISE_EXEC) mix test --cover --export-coverage default
@@ -2,8 +2,7 @@
title: "The Intention, Location, Details communication Pattern", title: "The Intention, Location, Details communication Pattern",
author: "Willem van den Ende", author: "Willem van den Ende",
tags: ~w(), tags: ~w(),
description: "Communicating our intent clearly is helpful in getting the results we want. I find the pattern “Intention, Location Details” as described by Emily Bache quite handy, It took me a while to find sources, I am collecting them here, before expanding on my own musings." description: "Communicating our intent clearly is helpful in getting the results we want. I find the pattern Intention, Location Details as described by Emily Bache quite handy, It took me a while to find sources, I am collecting them here, before expanding on my own musings.",
",
image: nil, image: nil,
published: true published: true
} }
@@ -1,6 +1,19 @@
defmodule FirehoseWeb.BlogControllerTest do defmodule FirehoseWeb.BlogControllerTest do
use FirehoseWeb.ConnCase, async: false use FirehoseWeb.ConnCase, async: false
# Content churns; the behavior under test does not. Pick deterministically
# from the compiled posts instead of pinning slugs - the pinned "hello-world"
# post was deleted in 1709cb7 and ten tests silently rotted with it.
defp a_draft_post do
Enum.find(Firehose.EngineeringBlog.all_posts(), &(&1.published == false)) ||
raise "no draft (published: false) engineering post to exercise status banners"
end
defp a_live_post do
Enum.find(Firehose.EngineeringBlog.all_posts(), &(Blogex.Post.visibility(&1) == :live)) ||
raise "no live engineering post to exercise og meta tags"
end
describe "GET /blog/:blog_id (index) - date filtering" do describe "GET /blog/:blog_id (index) - date filtering" do
test "does not show future-dated posts", %{conn: conn} do test "does not show future-dated posts", %{conn: conn} do
html = conn |> get(~p"/blog/engineering") |> html_response(200) html = conn |> get(~p"/blog/engineering") |> html_response(200)
@@ -26,7 +39,8 @@ defmodule FirehoseWeb.BlogControllerTest do
setup :register_and_log_in_user setup :register_and_log_in_user
test "authenticated user sees draft banner on draft post", %{conn: conn} do test "authenticated user sees draft banner on draft post", %{conn: conn} do
response = conn |> get(~p"/blog/engineering/hello-world") post = a_draft_post()
response = conn |> get(~p"/blog/engineering/#{post.id}")
assert html_response(response, 200) =~ "Draft" assert html_response(response, 200) =~ "Draft"
assert response.resp_body =~ "not published" assert response.resp_body =~ "not published"
@@ -49,9 +63,11 @@ defmodule FirehoseWeb.BlogControllerTest do
describe "GET /blog/:blog_id/:slug - no banners for unauthenticated" do describe "GET /blog/:blog_id/:slug - no banners for unauthenticated" do
test "unauthenticated user sees no banner on draft post", %{conn: conn} do test "unauthenticated user sees no banner on draft post", %{conn: conn} do
post = a_draft_post()
response = response =
conn conn
|> get(~p"/blog/engineering/hello-world") |> get(~p"/blog/engineering/#{post.id}")
|> html_response(200) |> html_response(200)
refute response =~ "post-status-banner" refute response =~ "post-status-banner"
@@ -69,26 +85,30 @@ defmodule FirehoseWeb.BlogControllerTest do
describe "GET /blog/:blog_id/:slug - Open Graph meta tags" do describe "GET /blog/:blog_id/:slug - Open Graph meta tags" do
test "meta tags include og_title", %{conn: conn} do test "meta tags include og_title", %{conn: conn} do
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200) post = a_live_post()
response = conn |> get(~p"/blog/engineering/#{post.id}") |> html_response(200)
assert response =~ ~s(<meta property="og:title") assert response =~ ~s(<meta property="og:title")
assert response =~ "Hello World" assert response =~ post.title
end end
test "meta tags include og_description", %{conn: conn} do test "meta tags include og_description", %{conn: conn} do
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200) post = a_live_post()
response = conn |> get(~p"/blog/engineering/#{post.id}") |> html_response(200)
assert response =~ ~s(<meta property="og:description") assert response =~ ~s(<meta property="og:description")
end end
test "meta tags include og_type article", %{conn: conn} do test "meta tags include og_type article", %{conn: conn} do
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200) post = a_live_post()
response = conn |> get(~p"/blog/engineering/#{post.id}") |> html_response(200)
assert response =~ ~s(<meta property="og:type" content="article") assert response =~ ~s(<meta property="og:type" content="article")
end end
test "meta tags include twitter:card", %{conn: conn} do test "meta tags include twitter:card", %{conn: conn} do
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200) post = a_live_post()
response = conn |> get(~p"/blog/engineering/#{post.id}") |> html_response(200)
assert response =~ ~s(<meta name="twitter:card" content="summary_large_image") assert response =~ ~s(<meta name="twitter:card" content="summary_large_image")
end end
@@ -101,10 +121,11 @@ defmodule FirehoseWeb.BlogControllerTest do
end end
test "meta tags have correct og_url", %{conn: conn} do test "meta tags have correct og_url", %{conn: conn} do
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200) post = a_live_post()
response = conn |> get(~p"/blog/engineering/#{post.id}") |> html_response(200)
assert response =~ assert response =~
~s(<meta property="og:url" content="http://localhost:4002/blog/engineering/hello-world") ~s(<meta property="og:url" content="http://localhost:4002/blog/engineering/#{post.id}")
end end
end end
@@ -1,12 +1,25 @@
defmodule FirehoseWeb.BlogTagsTest do defmodule FirehoseWeb.BlogTagsTest do
use FirehoseWeb.ConnCase use FirehoseWeb.ConnCase
# Content churns; the behavior under test does not. The newest live post
# with a tag is a deterministic stand-in, and tag pages sort date-descending,
# so it is on page 1 whatever else is published under that tag. (The pinned
# "Hello World" post was deleted in 1709cb7 and this test silently rotted.)
defp a_newest_live_post_with_tag do
Enum.find(Firehose.EngineeringBlog.all_posts(), fn post ->
Blogex.Post.visibility(post) == :live and post.tags != []
end) || raise "no live tagged engineering post to exercise the tag page"
end
describe "engineering blog tags" do describe "engineering blog tags" do
test "GET /blog/engineering/tag/:tag shows tag page with filtered posts", %{conn: conn} do test "GET /blog/engineering/tag/:tag shows tag page with filtered posts", %{conn: conn} do
body = conn |> get("/blog/engineering/tag/elixir") |> html_response(200) post = a_newest_live_post_with_tag()
assert body =~ ~s(tagged "elixir") tag = hd(post.tags)
body = conn |> get("/blog/engineering/tag/#{tag}") |> html_response(200)
assert body =~ ~s(tagged "#{tag}")
assert body =~ "Engineering Blog" assert body =~ "Engineering Blog"
assert body =~ "Hello World" assert body =~ post.title
end end
test "GET /blog/engineering/tag/:tag shows empty list for nonexistent tag", %{conn: conn} do test "GET /blog/engineering/tag/:tag shows empty list for nonexistent tag", %{conn: conn} do
@@ -9,6 +9,14 @@ defmodule FirehoseWeb.BlogTest do
body body
end end
# Content churns; the behavior under test does not. The newest live post is
# a deterministic stand-in - the pinned "hello-world" post was deleted in
# 1709cb7 and the slug tests silently rotted with it.
defp a_live_post do
Enum.find(Firehose.EngineeringBlog.all_posts(), &(Blogex.Post.visibility(&1) == :live)) ||
raise "no live engineering post to exercise the post pages"
end
describe "engineering blog (HTML)" do describe "engineering blog (HTML)" do
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
body = visit_blog_page(conn, "engineering") body = visit_blog_page(conn, "engineering")
@@ -17,8 +25,9 @@ defmodule FirehoseWeb.BlogTest do
end end
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
body = visit_blog_page(conn, "engineering", "/hello-world") post = a_live_post()
assert body =~ "Hello World" body = visit_blog_page(conn, "engineering", "/#{post.id}")
assert body =~ post.title
end end
test "GET /blog/engineering?page=2 shows the next page of older posts", %{conn: conn} do test "GET /blog/engineering?page=2 shows the next page of older posts", %{conn: conn} do
@@ -191,11 +200,16 @@ defmodule FirehoseWeb.BlogTest do
end end
test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do
assert %{"id" => "hello-world", "title" => "Hello World"} = post = a_live_post()
assert %{"id" => id, "title" => title} =
conn conn
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> get("/api/blog/engineering/hello-world") |> get("/api/blog/engineering/#{post.id}")
|> json_response(200) |> json_response(200)
assert id == post.id
assert title == post.title
end end
test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do
+27 -7
View File
@@ -3,39 +3,59 @@
MISE_BIN ?= mise MISE_BIN ?= mise
MISE_EXEC = $(MISE_BIN) exec -- MISE_EXEC = $(MISE_BIN) exec --
.PHONY: check precommit deps compile test test.coverage coverage crap format format.check verify .PHONY: check precommit deps deps.check compile test test.coverage coverage crap format format.check verify
# Cheap static gate for this library (no database, no coverage). Runs from the # Cheap static gate for this library (no database, no network, no coverage).
# root `make check`, which loops app then blogex. `format.check` only reports: # The deps.check guard fails loudly with a pointer at `make setup` when Hex
# deps are missing. Runs from the root `make check`, which loops app then
# blogex. `format.check` only reports:
# a gate that rewrites the files it judges can never fail. Fix formatting with # a gate that rewrites the files it judges can never fail. Fix formatting with
# `make format`. # `make format`.
# #
# Sensor slot: add `credo` once the "credo strict, tuned config (both apps)" # Sensor slot: add `credo` once the "credo strict, tuned config (both apps)"
# yak lands blogex's credo dep + .credo.exs. Do not list it before then — a # yak lands blogex's credo dep + .credo.exs. Do not list it before then — a
# gate that claims a check it did not run is worse than no gate. # gate that claims a check it did not run is worse than no gate.
check: compile format.check check: deps.check compile format.check
# Per-app precommit loop: same target name and alias shape as app/, so both # Per-app precommit loop: same target name and alias shape as app/, so both
# apps share one habit (compile --warning-as-errors, format, test). # apps share one habit (compile --warning-as-errors, format, test).
precommit: precommit:
$(MISE_EXEC) mix precommit $(MISE_EXEC) mix precommit
# Sync dependencies # Sync dependencies and compile them: NimblePublisher pulls in mdex_native,
# whose precompiled NIF downloads from GitHub at first compile. Keeping that
# in `deps` (called by root `make setup`) keeps check/test/verify offline on
# a cold clone.
deps: deps:
$(MISE_EXEC) mix deps.get $(MISE_EXEC) mix deps.get
$(MISE_EXEC) mix deps.compile
# Local, network-free guard: every package pinned in mix.lock must be present
# in deps/. check, test and test.coverage depend on this instead of `deps`, so
# the everyday loops never touch the network: `mix deps.get` contacts hex.pm
# even when everything is already installed (the advisory check), which fails
# in offline/sandboxed environments. Fetching is `make setup`'s explicit job,
# run from the repo root.
deps.check:
@test -f mix.lock || { echo "deps.check: no mix.lock - run 'make setup' at the repo root" >&2; exit 1; }
@missing=""; \
for dep in $$(sed -n 's/^[[:space:]]*"\([^"]*\)".*/\1/p' mix.lock); do \
[ -d "deps/$$dep" ] || missing="$$missing $$dep"; \
done; \
test -z "$$missing" || { echo "deps.check: missing:$$missing - run 'make setup' at the repo root" >&2; exit 1; }
# Compile the project (warnings are fatal) # Compile the project (warnings are fatal)
compile: compile:
$(MISE_EXEC) mix compile --warnings-as-errors $(MISE_EXEC) mix compile --warnings-as-errors
# Run the test suite # Run the test suite
test: deps test: deps.check
$(MISE_EXEC) mix test $(MISE_EXEC) mix test
# Run the suite once with coverage exported, so the coverage floor check (and, # Run the suite once with coverage exported, so the coverage floor check (and,
# later, CRAP) reads the export instead of running the suite a second time. # later, CRAP) reads the export instead of running the suite a second time.
# Stale exports are removed first: the floor must never report an older run. # Stale exports are removed first: the floor must never report an older run.
test.coverage: deps test.coverage: deps.check
rm -f cover/*.coverdata rm -f cover/*.coverdata
$(MISE_EXEC) mix test --cover --export-coverage default $(MISE_EXEC) mix test --cover --export-coverage default
+8 -6
View File
@@ -11,7 +11,9 @@
# tags composed from the pins (catches hand-edited image references). # tags composed from the pins (catches hand-edited image references).
# #
# Pure shell + sed: no mise, docker or elixir needed, so it runs in CI and in # Pure shell + sed: no mise, docker or elixir needed, so it runs in CI and in
# sandboxes before the toolchain is installed. # sandboxes before the toolchain is installed. Keep regexes POSIX-BRE: GNU
# extensions like \+ silently break under /usr/bin/sed on macOS (it treats
# \+ as a literal plus), which false-fails every pin as "not found".
set -euo pipefail set -euo pipefail
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
@@ -35,9 +37,9 @@ grab() { # grab <description> <key-regex> <file>
mise_elixir=$(grab 'mise elixir' 'elixir' "$mise_toml") mise_elixir=$(grab 'mise elixir' 'elixir' "$mise_toml")
mise_erlang=$(grab 'mise erlang' 'erlang' "$mise_toml") mise_erlang=$(grab 'mise erlang' 'erlang' "$mise_toml")
d_elixir=$(grab 'Dockerfile ELIXIR_VERSION' 'ARG[[:space:]]\+ELIXIR_VERSION' "$dockerfile") d_elixir=$(grab 'Dockerfile ELIXIR_VERSION' 'ARG[[:space:]]\{1,\}ELIXIR_VERSION' "$dockerfile")
d_otp=$(grab 'Dockerfile OTP_VERSION' 'ARG[[:space:]]\+OTP_VERSION' "$dockerfile") d_otp=$(grab 'Dockerfile OTP_VERSION' 'ARG[[:space:]]\{1,\}OTP_VERSION' "$dockerfile")
d_debian=$(grab 'Dockerfile DEBIAN_VERSION' 'ARG[[:space:]]\+DEBIAN_VERSION' "$dockerfile") d_debian=$(grab 'Dockerfile DEBIAN_VERSION' 'ARG[[:space:]]\{1,\}DEBIAN_VERSION' "$dockerfile")
if [ $status -eq 0 ]; then if [ $status -eq 0 ]; then
mise_elixir_ver=${mise_elixir%%-otp-*} mise_elixir_ver=${mise_elixir%%-otp-*}
@@ -59,7 +61,7 @@ if [ $status -eq 0 ]; then
# BUILDER_IMAGE must expand to the tag composed from the pins. # BUILDER_IMAGE must expand to the tag composed from the pins.
expected_builder="docker.io/hexpm/elixir:${d_elixir}-erlang-${d_otp}-debian-${d_debian}" expected_builder="docker.io/hexpm/elixir:${d_elixir}-erlang-${d_otp}-debian-${d_debian}"
d_builder=$(grab 'Dockerfile BUILDER_IMAGE' 'ARG[[:space:]]\+BUILDER_IMAGE' "$dockerfile") d_builder=$(grab 'Dockerfile BUILDER_IMAGE' 'ARG[[:space:]]\{1,\}BUILDER_IMAGE' "$dockerfile")
expanded_builder=$(printf '%s' "$d_builder" | expanded_builder=$(printf '%s' "$d_builder" |
sed -e "s/\${ELIXIR_VERSION}/$d_elixir/" \ sed -e "s/\${ELIXIR_VERSION}/$d_elixir/" \
-e "s/\${OTP_VERSION}/$d_otp/" \ -e "s/\${OTP_VERSION}/$d_otp/" \
@@ -69,7 +71,7 @@ if [ $status -eq 0 ]; then
# RUNNER_IMAGE must expand to the Debian pin. # RUNNER_IMAGE must expand to the Debian pin.
expected_runner="docker.io/debian:${d_debian}" expected_runner="docker.io/debian:${d_debian}"
d_runner=$(grab 'Dockerfile RUNNER_IMAGE' 'ARG[[:space:]]\+RUNNER_IMAGE' "$dockerfile") d_runner=$(grab 'Dockerfile RUNNER_IMAGE' 'ARG[[:space:]]\{1,\}RUNNER_IMAGE' "$dockerfile")
expanded_runner=${d_runner/\$\{DEBIAN_VERSION\}/$d_debian} expanded_runner=${d_runner/\$\{DEBIAN_VERSION\}/$d_debian}
[ "$expanded_runner" = "$expected_runner" ] || [ "$expanded_runner" = "$expected_runner" ] ||
die "RUNNER_IMAGE expands to $expanded_runner, expected $expected_runner" die "RUNNER_IMAGE expands to $expanded_runner, expected $expected_runner"