Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f714fe513d | ||
|
|
bb7dde1963 | ||
|
|
446380e3a9 | ||
|
|
e07f248cc8 | ||
|
|
90ac6e0825 |
@@ -2369,6 +2369,23 @@ Pre-commit hooks verify generated code is fresh — always run `make generate` a
|
|||||||
mistyped `feat` ships a minor version. `make release-dry` answers "what
|
mistyped `feat` ships a minor version. `make release-dry` answers "what
|
||||||
would this merge release" without pushing.
|
would this merge release" without pushing.
|
||||||
|
|
||||||
|
**The analyzer reads the type and ignores the scope, so a CI-only change
|
||||||
|
is `ci:` and never `fix(ci):`.** The scope is decoration; `fix` is a
|
||||||
|
patch whatever is in the brackets. Two commits touching nothing but
|
||||||
|
`.gitea/workflows/unclaim.yml` were written `fix(ci):` and cut `v0.2.1`
|
||||||
|
and `v0.2.2` — real releases, published to Arch, Homebrew and the APK
|
||||||
|
registry, containing no user-facing change. They were left in place
|
||||||
|
rather than deleted, because a version that vanishes is worse for
|
||||||
|
whoever pulled it than one that turns out to be empty.
|
||||||
|
|
||||||
|
**The blast radius is bigger than the version number**, which is what
|
||||||
|
makes this worth a paragraph. A merge to `main` starts two workflows;
|
||||||
|
if `release.yml` then pushes a tag, that tag push starts **four more**
|
||||||
|
(`arch-package`, `homebrew-formula`, `android-apk`, `desktop-assets`) —
|
||||||
|
on a runner with capacity 1, where the APK build alone is tens of
|
||||||
|
minutes. `make release-dry` before merging is how you find out, and it
|
||||||
|
is cheaper than every one of those.
|
||||||
|
|
||||||
**`@semantic-release/github` is not in that config and must not be.**
|
**`@semantic-release/github` is not in that config and must not be.**
|
||||||
Gitea's API is `/api/v1` and is not GitHub's surface, so
|
Gitea's API is `/api/v1` and is not GitHub's surface, so
|
||||||
`@semantic-release/exec` calls `scripts/gitea-release.sh` instead — one
|
`@semantic-release/exec` calls `scripts/gitea-release.sh` instead — one
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { test, expect } from '../support/fixtures.js';
|
import { test, expect } from '../support/fixtures.js';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far the scroll test scrolls. One constant, because the guard and
|
||||||
|
* the assertion have to agree about it — they did not, which is #133.
|
||||||
|
*/
|
||||||
|
const SCROLL_TARGET = 80;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plan 007 phase 5: expanding an album shows its tracks.
|
* Plan 007 phase 5: expanding an album shows its tracks.
|
||||||
*
|
*
|
||||||
@@ -104,20 +110,27 @@ test.describe('the album dropdown', () => {
|
|||||||
await app.setViewportSize({ width: 900, height: 600 });
|
await app.setViewportSize({ width: 900, height: 600 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expect.poll(() => scrollRange(app)).toMatchObject({
|
// Wait for the range the assertion below actually needs, not for
|
||||||
scrollable: true,
|
// "scrollable at all" (#133). The guard used to be
|
||||||
overflowY: 'auto',
|
// `scrollHeight > clientHeight + 40` while the next line asks to
|
||||||
});
|
// reach 80, so any range in 41-79 satisfied it and could not
|
||||||
|
// satisfy the assertion — and the grid passes through exactly
|
||||||
|
// that while it settles, because it recomputes its columns after
|
||||||
|
// the resize rather than during it. The settled range here is
|
||||||
|
// 330, so this waits rather than weakening anything.
|
||||||
|
await expect
|
||||||
|
.poll(() => scrollRange(app))
|
||||||
|
.toMatchObject({ room: true, overflowY: 'auto' });
|
||||||
|
|
||||||
await app.evaluate(() => {
|
await app.evaluate((target) => {
|
||||||
const sc = document
|
const sc = document
|
||||||
.querySelector('cover-grid')
|
.querySelector('cover-grid')
|
||||||
?.shadowRoot?.querySelector('.grid-scroll-container');
|
?.shadowRoot?.querySelector('.grid-scroll-container');
|
||||||
|
|
||||||
if (sc) sc.scrollTop = 80;
|
if (sc) sc.scrollTop = target;
|
||||||
});
|
}, SCROLL_TARGET);
|
||||||
|
|
||||||
expect(await scrollTop(app)).toBe(80);
|
expect(await scrollTop(app)).toBe(SCROLL_TARGET);
|
||||||
|
|
||||||
// And the dropdown it opens is on screen, wherever the manager
|
// And the dropdown it opens is on screen, wherever the manager
|
||||||
// decides that leaves the scroll. It is *not* "the position is
|
// decides that leaves the scroll. It is *not* "the position is
|
||||||
@@ -250,16 +263,19 @@ async function closeDropdown(app: Page): Promise<void> {
|
|||||||
|
|
||||||
/** Whether the grid can scroll at all, which decides if a probe can move. */
|
/** Whether the grid can scroll at all, which decides if a probe can move. */
|
||||||
async function scrollRange(app: Page) {
|
async function scrollRange(app: Page) {
|
||||||
return app.evaluate(() => {
|
return app.evaluate((target) => {
|
||||||
const sc = document
|
const sc = document
|
||||||
.querySelector('cover-grid')
|
.querySelector('cover-grid')
|
||||||
?.shadowRoot?.querySelector('.grid-scroll-container');
|
?.shadowRoot?.querySelector('.grid-scroll-container');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollable: !!sc && sc.scrollHeight > sc.clientHeight + 40,
|
// `room` is the precondition of the assertion that follows it:
|
||||||
|
// enough range to actually reach the target. A threshold below
|
||||||
|
// what the caller depends on is not a guard.
|
||||||
|
room: !!sc && sc.scrollHeight - sc.clientHeight >= target,
|
||||||
overflowY: sc ? getComputedStyle(sc).overflowY : '',
|
overflowY: sc ? getComputedStyle(sc).overflowY : '',
|
||||||
};
|
};
|
||||||
});
|
}, SCROLL_TARGET);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrollTop(app: Page): Promise<number> {
|
async function scrollTop(app: Page): Promise<number> {
|
||||||
|
|||||||
+6
-11
@@ -20,19 +20,14 @@ pre-commit:
|
|||||||
glob: "*.go"
|
glob: "*.go"
|
||||||
run: go tool golangci-lint run --timeout 5m ./...
|
run: go tool golangci-lint run --timeout 5m ./...
|
||||||
|
|
||||||
|
# Snapshots the tree either side of the generators and reports only
|
||||||
|
# what moved across them. This used to be `go generate` plus a bare
|
||||||
|
# `git diff --name-only`, which is the *whole unstaged worktree* — so
|
||||||
|
# any unrelated edit sitting there was reported as stale generated
|
||||||
|
# code, and `make generate` then fixed nothing. See the script.
|
||||||
codegen-check:
|
codegen-check:
|
||||||
glob: "*.{go,sql,templ}"
|
glob: "*.{go,sql,templ}"
|
||||||
run: |
|
run: ./scripts/codegen-check.sh
|
||||||
go generate ./...
|
|
||||||
if [ -n "$(git diff --name-only)" ]; then
|
|
||||||
echo "Generated code is out of date. Run 'make generate' and stage the changes."
|
|
||||||
# --no-pager, or this blocks forever on `less` waiting for a
|
|
||||||
# keypress that a hook run without a tty will never get: the
|
|
||||||
# commit hangs at exactly the moment it is trying to tell you
|
|
||||||
# why it failed.
|
|
||||||
git --no-pager diff --stat
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# frontend/bindings is generated by `wails3`, not `go generate`, so
|
# frontend/bindings is generated by `wails3`, not `go generate`, so
|
||||||
# the check above does not cover it. ~3.5s warm, ~20s on a cold
|
# the check above does not cover it. ~3.5s warm, ~20s on a cold
|
||||||
|
|||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Fails when `go generate ./...` would change something that is not staged.
|
||||||
|
#
|
||||||
|
# The obvious spelling of this is `go generate && git diff --name-only`,
|
||||||
|
# which is what the hook used to be, and it answers the wrong question:
|
||||||
|
# that diff is the *whole unstaged worktree*, so any unrelated edit — a
|
||||||
|
# note, a plan document, the next commit's files sitting there while this
|
||||||
|
# one lands — was reported as
|
||||||
|
#
|
||||||
|
# Generated code is out of date. Run 'make generate' and stage the changes.
|
||||||
|
#
|
||||||
|
# Running `make generate` then does nothing, because nothing generated is
|
||||||
|
# stale, and the message sends you looking for a codegen problem that does
|
||||||
|
# not exist. Splitting one piece of work into several commits is exactly
|
||||||
|
# the shape that triggers it, so the workaround was a constraint on commit
|
||||||
|
# order for no real reason.
|
||||||
|
#
|
||||||
|
# So the tree is snapshotted either side of the generators and only what
|
||||||
|
# *moved across them* is reported. That is deliberately not a list of
|
||||||
|
# generated paths: sqlcgen, `*_templ.go` and `frontend/src/events.ts` are
|
||||||
|
# today's answer, a fourth generator is one `//go:generate` line away, and
|
||||||
|
# a path list is a second place to remember it — the same reasoning that
|
||||||
|
# keeps staleshape.go parsing sql/schemas/ rather than restating it.
|
||||||
|
#
|
||||||
|
# Content, not names: a generated file that is *already* dirty and is then
|
||||||
|
# rewritten further keeps its name in both snapshots and would otherwise
|
||||||
|
# slip through.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# name + worktree blob hash for every file that differs from the index.
|
||||||
|
# A file listed but absent (a deletion) hashes as "gone" rather than
|
||||||
|
# aborting the pipeline.
|
||||||
|
snapshot() {
|
||||||
|
git diff --name-only | while IFS= read -r f; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
printf '%s %s\n' "$f" "$(git hash-object -- "$f")"
|
||||||
|
else
|
||||||
|
printf '%s gone\n' "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# A brand-new generated file is not in either diff, because it is not
|
||||||
|
# tracked at all — the same blind spot bindings-check.sh names. Both
|
||||||
|
# snapshots are taken before the generators run.
|
||||||
|
before="$(snapshot)"
|
||||||
|
before_untracked="$(git ls-files --others --exclude-standard)"
|
||||||
|
|
||||||
|
go generate ./...
|
||||||
|
|
||||||
|
after="$(snapshot)"
|
||||||
|
after_untracked="$(git ls-files --others --exclude-standard)"
|
||||||
|
|
||||||
|
# Symmetric difference, and the symmetry is the whole point. Generation
|
||||||
|
# can push a file *into* the unstaged set (it was current, now it is not)
|
||||||
|
# or *out* of it (someone hand-edited generated output and the generator
|
||||||
|
# put it back) — and the second is stale generated code just as much as
|
||||||
|
# the first. Comparing one direction only reports "current" for it,
|
||||||
|
# which is the failure this script was written to stop.
|
||||||
|
moved="$(comm -3 <(printf '%s\n' "$before" | sort) <(printf '%s\n' "$after" | sort) |
|
||||||
|
cut -d' ' -f1 | tr -d '\t' | sort -u | grep -v '^$' || true)"
|
||||||
|
|
||||||
|
if [ -n "$moved" ]; then
|
||||||
|
echo "codegen-check: generated code is out of date." >&2
|
||||||
|
echo "Run 'make generate' and stage:" >&2
|
||||||
|
printf ' %s\n' $moved >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$after_untracked" != "$before_untracked" ]; then
|
||||||
|
echo "codegen-check: generation produced new files. Stage them:" >&2
|
||||||
|
comm -13 <(printf '%s\n' "$before_untracked" | sort) \
|
||||||
|
<(printf '%s\n' "$after_untracked" | sort) >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "codegen-check: generated code is current"
|
||||||
Reference in New Issue
Block a user