Compare commits
5
Commits
8fa342e80d
...
7fda0455c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fda0455c1 | ||
|
|
0d344a4f57 | ||
|
|
84539ec40f | ||
|
|
97fc024a38 | ||
|
|
7215cd6db0 |
@@ -70,6 +70,12 @@ This is a Phoenix Liveview monorepo, the blogging library is in blogex, the appl
|
||||
|
||||
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` +
|
||||
format **check** (`mix format --check-formatted`), blogex compile (warnings
|
||||
fatal) + format **check**, then `scripts/check-pins.sh`. It never rewrites
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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:
|
||||
# this is the loop you run freely while working and before every commit.
|
||||
@@ -47,9 +47,18 @@ hooks:
|
||||
echo "installed $$hooks_dir/$$name -> $$hook"; \
|
||||
done
|
||||
|
||||
# Sync dependencies
|
||||
deps:
|
||||
@make -C app deps
|
||||
# One-time setup for this clone (and after any mix.exs gains a dependency):
|
||||
# fetches Hex deps for both apps and the app's JS packages. Needs network, and
|
||||
# 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:
|
||||
|
||||
+31
-8
@@ -3,12 +3,13 @@
|
||||
MISE_BIN ?= mise
|
||||
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
|
||||
# reports: a gate that rewrites the files it judges can never fail. Fix
|
||||
# formatting with `make format`.
|
||||
check: credo format.check
|
||||
# Run all static analysis checks (no database, no network required). The
|
||||
# deps.check guard fails loudly with a pointer at `make setup` when Hex deps
|
||||
# are missing. `format.check` only reports: a gate that rewrites the files it
|
||||
# judges can never fail. Fix formatting with `make format`.
|
||||
check: deps.check credo format.check
|
||||
|
||||
# Precommit target for CI/pre-commit hooks
|
||||
precommit: check compile
|
||||
@@ -23,9 +24,31 @@ assets:
|
||||
@test -f assets/node_modules/.package-lock.json || \
|
||||
(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
|
||||
$(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:
|
||||
@@ -33,13 +56,13 @@ compile:
|
||||
|
||||
# Run tests (requires PostgreSQL running on localhost:5432)
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
# 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
|
||||
$(MISE_EXEC) mix test --cover --export-coverage default
|
||||
|
||||
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
title: "The Intention, Location, Details communication Pattern",
|
||||
author: "Willem van den Ende",
|
||||
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,
|
||||
published: true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
defmodule FirehoseWeb.BlogControllerTest do
|
||||
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
|
||||
test "does not show future-dated posts", %{conn: conn} do
|
||||
html = conn |> get(~p"/blog/engineering") |> html_response(200)
|
||||
@@ -26,7 +39,8 @@ defmodule FirehoseWeb.BlogControllerTest do
|
||||
setup :register_and_log_in_user
|
||||
|
||||
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 response.resp_body =~ "not published"
|
||||
@@ -49,9 +63,11 @@ defmodule FirehoseWeb.BlogControllerTest do
|
||||
|
||||
describe "GET /blog/:blog_id/:slug - no banners for unauthenticated" do
|
||||
test "unauthenticated user sees no banner on draft post", %{conn: conn} do
|
||||
post = a_draft_post()
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get(~p"/blog/engineering/hello-world")
|
||||
|> get(~p"/blog/engineering/#{post.id}")
|
||||
|> html_response(200)
|
||||
|
||||
refute response =~ "post-status-banner"
|
||||
@@ -69,26 +85,30 @@ defmodule FirehoseWeb.BlogControllerTest do
|
||||
|
||||
describe "GET /blog/:blog_id/:slug - Open Graph meta tags" 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 =~ "Hello World"
|
||||
assert response =~ post.title
|
||||
end
|
||||
|
||||
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")
|
||||
end
|
||||
|
||||
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")
|
||||
end
|
||||
|
||||
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")
|
||||
end
|
||||
@@ -101,10 +121,11 @@ defmodule FirehoseWeb.BlogControllerTest do
|
||||
end
|
||||
|
||||
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 =~
|
||||
~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
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
defmodule FirehoseWeb.BlogTagsTest do
|
||||
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
|
||||
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)
|
||||
assert body =~ ~s(tagged "elixir")
|
||||
post = a_newest_live_post_with_tag()
|
||||
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 =~ "Hello World"
|
||||
assert body =~ post.title
|
||||
end
|
||||
|
||||
test "GET /blog/engineering/tag/:tag shows empty list for nonexistent tag", %{conn: conn} do
|
||||
|
||||
@@ -9,6 +9,14 @@ defmodule FirehoseWeb.BlogTest do
|
||||
body
|
||||
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
|
||||
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
|
||||
body = visit_blog_page(conn, "engineering")
|
||||
@@ -17,8 +25,9 @@ defmodule FirehoseWeb.BlogTest do
|
||||
end
|
||||
|
||||
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
|
||||
body = visit_blog_page(conn, "engineering", "/hello-world")
|
||||
assert body =~ "Hello World"
|
||||
post = a_live_post()
|
||||
body = visit_blog_page(conn, "engineering", "/#{post.id}")
|
||||
assert body =~ post.title
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get("/api/blog/engineering/hello-world")
|
||||
|> get("/api/blog/engineering/#{post.id}")
|
||||
|> json_response(200)
|
||||
|
||||
assert id == post.id
|
||||
assert title == post.title
|
||||
end
|
||||
|
||||
test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do
|
||||
|
||||
+27
-7
@@ -3,39 +3,59 @@
|
||||
MISE_BIN ?= mise
|
||||
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
|
||||
# root `make check`, which loops app then blogex. `format.check` only reports:
|
||||
# Cheap static gate for this library (no database, no network, no coverage).
|
||||
# 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
|
||||
# `make format`.
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# apps share one habit (compile --warning-as-errors, format, test).
|
||||
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:
|
||||
$(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:
|
||||
$(MISE_EXEC) mix compile --warnings-as-errors
|
||||
|
||||
# Run the test suite
|
||||
test: deps
|
||||
test: deps.check
|
||||
$(MISE_EXEC) mix test
|
||||
|
||||
# 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.
|
||||
# Stale exports are removed first: the floor must never report an older run.
|
||||
test.coverage: deps
|
||||
test.coverage: deps.check
|
||||
rm -f cover/*.coverdata
|
||||
$(MISE_EXEC) mix test --cover --export-coverage default
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
# 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
|
||||
# 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
|
||||
|
||||
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_erlang=$(grab 'mise erlang' 'erlang' "$mise_toml")
|
||||
d_elixir=$(grab 'Dockerfile ELIXIR_VERSION' 'ARG[[:space:]]\+ELIXIR_VERSION' "$dockerfile")
|
||||
d_otp=$(grab 'Dockerfile OTP_VERSION' 'ARG[[:space:]]\+OTP_VERSION' "$dockerfile")
|
||||
d_debian=$(grab 'Dockerfile DEBIAN_VERSION' 'ARG[[:space:]]\+DEBIAN_VERSION' "$dockerfile")
|
||||
d_elixir=$(grab 'Dockerfile ELIXIR_VERSION' 'ARG[[:space:]]\{1,\}ELIXIR_VERSION' "$dockerfile")
|
||||
d_otp=$(grab 'Dockerfile OTP_VERSION' 'ARG[[:space:]]\{1,\}OTP_VERSION' "$dockerfile")
|
||||
d_debian=$(grab 'Dockerfile DEBIAN_VERSION' 'ARG[[:space:]]\{1,\}DEBIAN_VERSION' "$dockerfile")
|
||||
|
||||
if [ $status -eq 0 ]; then
|
||||
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.
|
||||
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" |
|
||||
sed -e "s/\${ELIXIR_VERSION}/$d_elixir/" \
|
||||
-e "s/\${OTP_VERSION}/$d_otp/" \
|
||||
@@ -69,7 +71,7 @@ if [ $status -eq 0 ]; then
|
||||
|
||||
# RUNNER_IMAGE must expand to the Debian pin.
|
||||
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" = "$expected_runner" ] ||
|
||||
die "RUNNER_IMAGE expands to $expanded_runner, expected $expected_runner"
|
||||
|
||||
Reference in New Issue
Block a user