Skip to content

Commit c202bb4

Browse files
committed
chore: Add pre-push hook and clean up build duplication
1 parent b749d11 commit c202bb4

6 files changed

Lines changed: 134 additions & 50 deletions

File tree

.githooks/pre-push

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env sh
2+
#
3+
# Runs `make check-all` — the same lint, type, test, distribution, and sample-app checks that
4+
# CI runs — before allowing a push. The checks themselves live in the Makefile so that this
5+
# hook and CI cannot drift apart; this script only adds the git-specific parts.
6+
#
7+
# Install it with `make hooks` (which points core.hooksPath at this directory).
8+
# Bypass it for a single push with `git push --no-verify`.
9+
#
10+
# Note: this checks the working tree, not the commits being pushed. Uncommitted changes are
11+
# included, which is usually what you want and occasionally surprising.
12+
13+
set -eu
14+
15+
# --- skip when the push only deletes remote refs -----------------------------------------
16+
# git feeds "<local ref> <local sha> <remote ref> <remote sha>" per ref on stdin. A deletion
17+
# has an all-zero local sha and nothing to check. Skip the read entirely when run from a
18+
# terminal, so the hook stays runnable by hand.
19+
if [ ! -t 0 ]; then
20+
saw_ref=0
21+
only_deletes=1
22+
while read -r _local_ref local_sha _remote_ref _remote_sha; do
23+
saw_ref=1
24+
case "$local_sha" in
25+
*[!0]*) only_deletes=0 ;;
26+
esac
27+
done
28+
if [ "$saw_ref" -eq 1 ] && [ "$only_deletes" -eq 1 ]; then
29+
echo "pre-push: deleting refs only, nothing to check."
30+
exit 0
31+
fi
32+
fi
33+
34+
cd "$(git rev-parse --show-toplevel)"
35+
36+
for tool in make uv; do
37+
if ! command -v "$tool" >/dev/null 2>&1; then
38+
echo "pre-push: $tool is not installed, cannot run the checks."
39+
[ "$tool" = uv ] && echo " see https://docs.astral.sh/uv/getting-started/installation/"
40+
exit 1
41+
fi
42+
done
43+
44+
if ! make check-all; then
45+
printf '\n%s\n' "pre-push FAILED. Fix the problem above, or bypass with: git push --no-verify"
46+
exit 1
47+
fi

.github/workflows/ci.yml

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ on:
88
permissions:
99
contents: read
1010

11+
# The checks themselves live in the Makefile so that CI and the pre-push hook cannot drift
12+
# apart. bash everywhere keeps the make recipes portable across the runner images.
13+
defaults:
14+
run:
15+
shell: bash
16+
1117
jobs:
1218
lint:
1319
name: Lint + Typecheck
@@ -20,17 +26,9 @@ jobs:
2026
with:
2127
enable-cache: true
2228

23-
- name: uv sync
24-
run: uv sync --all-extras --frozen
25-
26-
- name: ruff check
27-
run: uv run ruff check .
28-
29-
- name: ruff format --check
30-
run: uv run ruff format --check .
31-
32-
- name: mypy
33-
run: uv run mypy
29+
- run: make verify-lock
30+
- run: make lint
31+
- run: make typecheck
3432

3533
test:
3634
name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
@@ -54,11 +52,8 @@ jobs:
5452
enable-cache: true
5553
python-version: ${{ matrix.python-version }}
5654

57-
- name: uv sync
58-
run: uv sync --all-extras --frozen
59-
60-
- name: pytest
61-
run: uv run pytest --cov --cov-report=term-missing
55+
- run: make verify-lock
56+
- run: make coverage
6257

6358
build:
6459
name: Build distribution
@@ -71,17 +66,8 @@ jobs:
7166
with:
7267
enable-cache: true
7368

74-
- name: uv build
75-
run: uv build
76-
77-
- name: Check distribution metadata
78-
run: uvx twine check dist/*
79-
80-
- name: Verify the wheel imports cleanly
81-
run: |
82-
uv venv /tmp/smoke
83-
VIRTUAL_ENV=/tmp/smoke uv pip install dist/*.whl
84-
/tmp/smoke/bin/python -c "import configdirector; print(configdirector.__version__)"
69+
# Builds into dist/, checks the metadata, and imports the wheel in a clean environment.
70+
- run: make dist-check
8571

8672
- uses: actions/upload-artifact@v4
8773
with:
@@ -91,10 +77,6 @@ jobs:
9177
samples:
9278
name: Sample apps
9379
runs-on: ubuntu-latest
94-
strategy:
95-
fail-fast: false
96-
matrix:
97-
sample: [flask]
9880
steps:
9981
- uses: actions/checkout@v5
10082

@@ -103,15 +85,5 @@ jobs:
10385
with:
10486
enable-cache: true
10587

106-
# Samples resolve the SDK from a local path, so they are not locked against a release.
107-
- name: uv sync
108-
run: uv sync
109-
working-directory: ./samples/${{ matrix.sample }}
110-
111-
- name: mypy
112-
run: uv run mypy
113-
working-directory: ./samples/${{ matrix.sample }}
114-
115-
- name: pytest
116-
run: uv run pytest
117-
working-directory: ./samples/${{ matrix.sample }}
88+
# Each sample resolves the SDK by local path, so it is not locked against a release.
89+
- run: make samples

Makefile

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
.PHONY: install lint format typecheck test coverage build clean check
1+
.PHONY: install hooks lint format typecheck test coverage build verify-lock dist-check samples check check-all clean
22

33
install:
44
uv sync --all-extras
55

6+
hooks:
7+
git config core.hooksPath .githooks
8+
@echo "pre-push hook installed. Bypass a single push with 'git push --no-verify'."
9+
10+
# Asserts uv.lock is still consistent with pyproject.toml, which is what CI installs from.
11+
verify-lock:
12+
@printf '\n\033[1m==> verify-lock\033[0m\n'
13+
uv sync --all-extras --locked
14+
615
lint:
16+
@printf '\n\033[1m==> lint\033[0m\n'
717
uv run ruff check .
818
uv run ruff format --check .
919

@@ -12,19 +22,49 @@ format:
1222
uv run ruff format .
1323

1424
typecheck:
25+
@printf '\n\033[1m==> typecheck\033[0m\n'
1526
uv run mypy
1627

1728
test:
29+
@printf '\n\033[1m==> test\033[0m\n'
1830
uv run pytest
1931

2032
coverage:
33+
@printf '\n\033[1m==> coverage\033[0m\n'
2134
uv run pytest --cov --cov-report=term-missing
2235

2336
build:
37+
@printf '\n\033[1m==> build\033[0m\n'
38+
rm -rf dist
2439
uv build
2540

41+
# Validates the built distribution's metadata and that the wheel imports on its own, in a
42+
# throwaway environment. Leaves dist/ in place so CI can upload it.
43+
dist-check: build
44+
@printf '\n\033[1m==> dist-check\033[0m\n'
45+
uvx twine check dist/*
46+
@tmp=$$(mktemp -d); \
47+
uv venv "$$tmp/venv" --quiet \
48+
&& VIRTUAL_ENV="$$tmp/venv" uv pip install --quiet dist/*.whl \
49+
&& VIRTUAL_ENV="$$tmp/venv" uv run --no-project python -c \
50+
"import configdirector; print('wheel imports cleanly:', configdirector.__version__)"; \
51+
status=$$?; rm -rf "$$tmp"; exit $$status
52+
53+
# Samples resolve the SDK by local path, so this is what catches a breaking API change.
54+
samples:
55+
@for sample in samples/*/; do \
56+
[ -f "$$sample/pyproject.toml" ] || continue; \
57+
printf '\n\033[1m==> sample %s\033[0m\n' "$$(basename $$sample)"; \
58+
(cd "$$sample" && uv sync --quiet && uv run mypy && uv run pytest) || exit 1; \
59+
done
60+
61+
# The fast loop while working.
2662
check: lint typecheck test
2763

64+
# Everything CI runs. The pre-push hook calls this.
65+
check-all: verify-lock lint typecheck test dist-check samples
66+
@printf '\n\033[1m✓ all checks passed\033[0m\n'
67+
2868
clean:
2969
rm -rf dist build .pytest_cache .mypy_cache .ruff_cache .coverage htmlcov
3070
find . -type d -name __pycache__ -prune -exec rm -rf {} +

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,38 @@ This project uses [uv](https://docs.astral.sh/uv/).
167167

168168
```bash
169169
make install # uv sync --all-extras
170+
make hooks # install the pre-push hook (once, per clone)
171+
170172
make test # pytest
171173
make lint # ruff check + ruff format --check
172174
make typecheck # mypy
173-
make check # lint + typecheck + test
175+
make samples # typecheck and test every app under samples/
174176
make build # build the sdist and wheel
177+
make dist-check # build, validate the metadata, import the wheel in a clean env
178+
179+
make check # lint + typecheck + test — the fast loop
180+
make check-all # everything CI runs
181+
```
182+
183+
CI and the pre-push hook both call these targets rather than repeating the commands, so the
184+
three cannot drift apart.
185+
186+
### Pre-push hook
187+
188+
`make hooks` points `core.hooksPath` at [`.githooks/`](.githooks/), so
189+
[`.githooks/pre-push`](.githooks/pre-push) runs `make check-all` before every push: lockfile,
190+
lint, formatting, types, tests, a real distribution build, and every sample app. It takes a few
191+
seconds.
192+
193+
Pushes that only delete remote refs skip the checks. To bypass it for a single push:
194+
195+
```bash
196+
git push --no-verify
175197
```
176198

199+
Note that the hook checks your working tree, not the commits being pushed, so uncommitted
200+
changes count.
201+
177202
## Documentation
178203

179204
Refer to the [official documentation for the Python SDK](https://docs.configdirector.com/sdks/server/python).

samples/flask/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
CONFIGDIRECTOR_SERVER_KEY=your-server-sdk-key
44

55
# Verbosity of the SDK's own logger: DEBUG, INFO, WARNING, ERROR.
6-
# The sample defaults to DEBUG so you can watch every evaluation; set INFO to quiet it down.
7-
CONFIGDIRECTOR_LOG_LEVEL=DEBUG
6+
# Set DEBUG to watch every evaluation as it happens.
7+
CONFIGDIRECTOR_LOG_LEVEL=INFO

samples/flask/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ lands in the application's logging namespace rather than the SDK's:
8787

8888
```python
8989
sdk_logger = logging.getLogger("flask_sample.configdirector")
90-
sdk_logger.setLevel(os.environ.get("CONFIGDIRECTOR_LOG_LEVEL", "DEBUG"))
90+
sdk_logger.setLevel(os.environ.get("CONFIGDIRECTOR_LOG_LEVEL", "INFO"))
9191

9292
client = ConfigDirectorClient(..., logger=sdk_logger)
9393
```
@@ -99,8 +99,8 @@ flask_sample.configdirector DEBUG No config state found for 'integer-config', re
9999
Any object with `debug`/`info`/`warning`/`error` methods works — a stdlib `Logger` satisfies
100100
that, and so does `create_console_logger("debug")` if you would rather not configure the
101101
`logging` module at all. Omit `logger=` entirely and the SDK falls back to the standard library
102-
logger named `configdirector`. The sample defaults to `DEBUG` so you can watch every evaluation
103-
as it happens; set `CONFIGDIRECTOR_LOG_LEVEL=INFO` to quiet it down.
102+
logger named `configdirector`. Set `CONFIGDIRECTOR_LOG_LEVEL=DEBUG` to watch every evaluation as
103+
it happens.
104104

105105
**Shutdown is clean.** `atexit` closes the client, dropping connections and flushing pending
106106
telemetry. A production deployment would also hook its server's worker-exit signal.

0 commit comments

Comments
 (0)