Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,36 @@ jobs:
run: |
touched=$(git diff --name-only "$BEFORE" "$SHA" -- src/content 2>/dev/null || true)
[ -z "$touched" ] && { echo "No generated content touched."; exit 0; }
author=$(git log -1 --format='%an' "$SHA")
if [ "$author" = "virtualddd-sync" ]; then

# Ask who authored the commits that *touched src/content* in this
# range — not who authored the tip.
#
# It used to read `git log -1 "$SHA"`, which is the same thing only
# when the sync's own commit is the very last one to land. Merge a
# pull request that carries a sync commit and the tip is the merge
# commit, authored by whoever pressed the button; the content is
# untouched by them and the deploy fails anyway. That is a false
# positive on the one workflow whose whole job is to be trusted, and
# the obvious workaround for it — push straight to main — is worse
# than the rule being enforced.
#
# A squash still fails, and correctly: squashing rewrites the author
# of the content into whoever opened the pull request, so there is no
# longer any evidence that the sync produced it. Use a rebase or a
# merge commit when a branch carries synced content.
authors=$(git log --format='%an' "$BEFORE..$SHA" -- src/content | sort -u)
# Fail closed when there is nobody to attribute it to. A force-push
# backwards leaves `github.event.before` newer than `github.sha`, so
# the range is empty while the diff above still shows content moving:
# an empty author list would otherwise read as "nobody but the sync"
# and wave it through. A guard whose whole job is attribution must not
# pass when it cannot attribute.
if [ -z "$authors" ]; then
echo "::error::Content under src/content/ changed between $BEFORE and $SHA, but no commit in that range touched it. Cannot tell who did; refusing to deploy."
exit 1
fi
others=$(printf '%s\n' "$authors" | grep -v '^virtualddd-sync$' || true)
if [ -z "$others" ]; then
echo "Changed by the sync, which is the only thing allowed to."
exit 0
fi
Expand All @@ -123,10 +151,16 @@ jobs:
echo
echo "$touched" | sed 's/^/ /'
echo
echo "Changed by: $(printf '%s' "$others" | paste -sd, | sed 's/,/, /g')"
echo
echo "Make the change in Notion instead. If the *shape* of the content needs to"
echo "change, that is a schema change; see docs/content-model.md."
echo
echo "If this *is* the sync's output arriving through a pull request, merge it"
echo "with a rebase or a merge commit rather than a squash — a squash rewrites"
echo "the author and erases the evidence that the sync produced it."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Generated content under src/content/ was edited by hand (author: $author). Change it in Notion."
echo "::error::Generated content under src/content/ was changed by $(printf '%s' "$others" | paste -sd, | sed 's/,/, /g'). Change it in Notion."
exit 1

- uses: actions/setup-node@v7
Expand All @@ -150,6 +184,21 @@ jobs:

- name: Contracts, redirects and browser behaviour
run: |
# `node --test` on a glob that matches nothing prints "pass 0" and
# exits 0. Both globs match today, so this changes nothing today —
# it is here because the sibling repository this pipeline was ported
# to spent an entire migration with an empty `tests/` directory,
# reporting success on every push while running no blocking tests at
# all, and nothing said so.
#
# A green tick for "there was nothing to check" is worse than a red
# one, because it is indistinguishable from "everything passed".
for glob in "tests/unit/*.test.mjs" "tests/*.test.mjs"; do
compgen -G "$glob" > /dev/null || {
echo "::error::No test files match $glob — the suite is empty, not passing."
exit 1
}
done
npx playwright install --with-deps chromium
node --import tsx --test "tests/unit/*.test.mjs"
node --test "tests/*.test.mjs"
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,35 @@ jobs:

- run: npm ci

# Every other secret in this repository degrades to "skip and say so" —
# deploy.yml guards on `[ -z "$HOST" ]`. This one did not: an empty or
# rotated `NOTION_TOKEN` makes the sync script exit 1, on a schedule,
# hourly, with nothing actually wrong. A red run that means "not
# configured" is indistinguishable from a red run that means "broken",
# and teaches everyone to ignore both.
#
# Skipping the *job* rather than the step, because everything after this
# point — the diff, the commit, the deploy it triggers — only means
# anything if something was fetched.
- name: Is there anything to sync from?
id: token
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
run: |
if [ -z "$NOTION_TOKEN" ]; then
echo "ready=false" >> "$GITHUB_OUTPUT"
{
echo "### Skipped"
echo
echo "No \`NOTION_TOKEN\` secret is set, so there is nothing to sync from."
echo "Add it at **Settings → Secrets and variables → Actions**, then re-run."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "ready=true" >> "$GITHUB_OUTPUT"
fi

- name: Pull Notion and ddd-crew
if: steps.token.outputs.ready == 'true'
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
# Raises GitHub's 60/hour unauthenticated limit, which CI runners
Expand Down Expand Up @@ -106,6 +134,9 @@ jobs:

- name: What changed?
id: diff
# Guarded too, or a skipped sync reports "nothing changed in Notion",
# which is a different claim from \"we never looked\".
if: steps.token.outputs.ready == 'true'
run: |
# Stage first, and ask the index rather than the working tree.
# `git diff` cannot see a file git has never heard of, and that is
Expand Down