Skip to content

Commit b273b71

Browse files
github-actions[bot]batonogovclaude
authored
Pin uv 0.10.0, add Taskfile and pre-push hook, improve tests (#192)
* Pin uv 0.10.0, add Taskfile and pre-push hook, improve tests - Pin uv version to 0.10.0 instead of latest - Add Taskfile.yml with build/test/setup tasks - Add .githooks/pre-push to run tests before push - Rewrite test/main.py: remove flaky network calls, add assertions, use Flask test client, exit with non-zero code on failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use Taskfile in CI for Linux image tests Replace build-and-test.sh with task test:linux/test:linux-slim in CI workflow. This adds binary verification and keeps CI in sync with local pre-push hook. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove build-and-test.sh, use Taskfile for all CI tests - Add build:windows and test:windows tasks to Taskfile - Migrate windows CI job to use task test:windows - Update commented-out osx CI job to reference Taskfile - Include windows in `task test` (all images) - Delete build-and-test.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix multi-platform builds: uv on amd64/arm64, pip fallback on others COPY --from=ghcr.io/astral-sh/uv fails on platforms not in the uv manifest (386, arm/v5, arm/v7, s390x, ppc64le, riscv64). Replace with conditional install via TARGETARCH: pip-install uv on amd64/arm64, plain pip elsewhere. Entrypoint detects uv at runtime for requirements and PyPI mirror config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Pin arduino/setup-task to full commit SHA for Sonar S7637 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Федор Батоногов <fekinos@me.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 40922c1 commit b273b71

File tree

8 files changed

+155
-113
lines changed

8 files changed

+155
-113
lines changed

.githooks/pre-push

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
echo "Running Docker build & test before push..."
6+
7+
if ! command -v task &>/dev/null; then
8+
echo "Error: 'task' (go-task) is not installed. Skipping tests."
9+
echo "Install: https://taskfile.dev/installation/"
10+
exit 0
11+
fi
12+
13+
task test
14+
15+
echo "All tests passed."

.github/workflows/test.yml

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,45 @@ jobs:
1212
steps:
1313
- name: Checkout Repo
1414
uses: actions/checkout@v6.0.2
15+
- name: Install Task
16+
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
17+
with:
18+
version: 3.x
1519
- name: Run Tests
16-
run: |
17-
echo "Building image to test"
18-
./build-and-test.sh Dockerfile-py3-windows
20+
run: task test:windows
1921

2022
linux:
2123
runs-on: ubuntu-latest
2224
steps:
2325
- name: Checkout Repo
2426
uses: actions/checkout@v6.0.2
27+
- name: Install Task
28+
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
29+
with:
30+
version: 3.x
2531
- name: Run Tests
26-
run: |
27-
echo "Building image to test"
28-
./build-and-test.sh Dockerfile-py3-linux
32+
run: task test:linux
2933

3034
linux-slim:
3135
runs-on: ubuntu-latest
3236
steps:
3337
- name: Checkout Repo
3438
uses: actions/checkout@v6.0.2
39+
- name: Install Task
40+
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
41+
with:
42+
version: 3.x
3543
- name: Run Tests
36-
run: |
37-
echo "Building image to test"
38-
./build-and-test.sh Dockerfile-py3-linux-slim
44+
run: task test:linux-slim
3945

4046
# osx:
4147
# runs-on: ubuntu-latest
4248
# steps:
4349
# - name: Checkout Repo
4450
# uses: actions/checkout@v6.0.2
51+
# - name: Install Task
52+
# uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
53+
# with:
54+
# version: 3.x
4555
# - name: Run Tests
46-
# run: |
47-
# echo "Building image to test"
48-
# ./build-and-test.sh Dockerfile-py3-osx
56+
# run: task test:osx

Dockerfile-py3-linux

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ ENV PYTHONUNBUFFERED=1
1515
# Copy the entrypoint script
1616
COPY entrypoint-linux.sh /entrypoint.sh
1717

18-
# Install uv
19-
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
20-
21-
# Install dependencies, PyInstaller, and set execute permissions for the entrypoint
22-
RUN uv pip install --system --no-cache pyinstaller==$PYINSTALLER_VERSION \
23-
&& chmod +x /entrypoint.sh
18+
# Install PyInstaller (use uv on amd64/arm64, pip on other platforms)
19+
ARG TARGETARCH
20+
RUN if [ "$TARGETARCH" = "amd64" ] || [ "$TARGETARCH" = "arm64" ]; then \
21+
pip install --no-cache-dir uv==0.10.0 && \
22+
uv pip install --system --no-cache pyinstaller==$PYINSTALLER_VERSION; \
23+
else \
24+
pip install --no-cache-dir pyinstaller==$PYINSTALLER_VERSION; \
25+
fi && \
26+
chmod +x /entrypoint.sh
2427

2528
# Set the working directory and mount the volume
2629
VOLUME /src/

Dockerfile-py3-linux-slim

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@ ENV PYTHONUNBUFFERED=1
1717
# Copy entrypoint script early to ensure it's available before dependency installation
1818
COPY entrypoint-linux.sh /entrypoint.sh
1919

20-
# Install uv
21-
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
22-
2320
# Install dependencies and PyInstaller in a single RUN command to reduce image layers
21+
ARG TARGETARCH
2422
RUN apt update && \
2523
apt install --no-install-recommends -y \
2624
binutils \
2725
gcc \
2826
zlib1g-dev && \
2927
rm -rf /var/lib/apt/lists/* && \
30-
uv pip install --system --no-cache pyinstaller==$PYINSTALLER_VERSION && \
28+
if [ "$TARGETARCH" = "amd64" ] || [ "$TARGETARCH" = "arm64" ]; then \
29+
pip install --no-cache-dir uv==0.10.0 && \
30+
uv pip install --system --no-cache pyinstaller==$PYINSTALLER_VERSION; \
31+
else \
32+
pip install --no-cache-dir pyinstaller==$PYINSTALLER_VERSION; \
33+
fi && \
3134
chmod +x /entrypoint.sh
3235

3336
# Set the working directory and create a volume for source code

Taskfile.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
version: '3'
2+
3+
vars:
4+
TEST_DIR: ./test
5+
IMAGE_PREFIX: pyinstaller-test
6+
7+
tasks:
8+
build:windows:
9+
desc: Build Windows image
10+
cmds:
11+
- docker build -f Dockerfile-py3-windows -t {{.IMAGE_PREFIX}}-windows .
12+
13+
build:linux:
14+
desc: Build Linux image
15+
cmds:
16+
- docker build -f Dockerfile-py3-linux -t {{.IMAGE_PREFIX}}-linux .
17+
18+
build:linux-slim:
19+
desc: Build Linux slim image
20+
cmds:
21+
- docker build -f Dockerfile-py3-linux-slim -t {{.IMAGE_PREFIX}}-linux-slim .
22+
23+
test:windows:
24+
desc: Build and test Windows image
25+
cmds:
26+
- task: build:windows
27+
- docker run --rm -v "$(pwd)/{{.TEST_DIR}}:/src/" {{.IMAGE_PREFIX}}-windows "pyinstaller main.py --onefile"
28+
29+
test:linux:
30+
desc: Build and test Linux image
31+
cmds:
32+
- task: build:linux
33+
- docker run --rm -v "$(pwd)/{{.TEST_DIR}}:/src/" {{.IMAGE_PREFIX}}-linux "pyinstaller main.py --onefile"
34+
- docker run --rm -v "$(pwd)/{{.TEST_DIR}}:/src/" {{.IMAGE_PREFIX}}-linux ./dist/main
35+
36+
test:linux-slim:
37+
desc: Build and test Linux slim image
38+
cmds:
39+
- task: build:linux-slim
40+
- docker run --rm -v "$(pwd)/{{.TEST_DIR}}:/src/" {{.IMAGE_PREFIX}}-linux-slim "pyinstaller main.py --onefile"
41+
- docker run --rm -v "$(pwd)/{{.TEST_DIR}}:/src/" {{.IMAGE_PREFIX}}-linux-slim ./dist/main
42+
43+
test:
44+
desc: Build and test all images
45+
cmds:
46+
- task: test:windows
47+
- task: test:linux
48+
- task: test:linux-slim
49+
50+
setup:
51+
desc: Configure git hooks
52+
cmds:
53+
- git config core.hooksPath .githooks

build-and-test.sh

Lines changed: 0 additions & 26 deletions
This file was deleted.

entrypoint-linux.sh

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,37 @@ WORKDIR=${SRCDIR:-/src}
1919
# should we build.
2020
# In case it's not defind, find the first match for `*.spec`
2121
SPECFILE=${SPECFILE:-$(find . -maxdepth 1 -type f -name '*.spec' -print -quit)}
22+
23+
# Choose installer: uv when available (amd64/arm64), pip otherwise
24+
if command -v uv &>/dev/null; then
25+
PIP_INSTALL="uv pip install --system"
26+
else
27+
PIP_INSTALL="pip3 install"
28+
fi
29+
2230
# In case the user specified a custom URL for PYPI, then use
2331
# that one, instead of the default one.
24-
2532
if [[ "$PYPI_URL" != "https://pypi.python.org/" ]] || \
2633
[[ "$PYPI_INDEX_URL" != "https://pypi.python.org/simple" ]]; then
27-
export UV_INDEX_URL="$PYPI_INDEX_URL"
28-
# Extract hostname to allow insecure (non-HTTPS) private mirrors
29-
export UV_INSECURE_HOST="$(echo $PYPI_URL | perl -pe 's|^.*?://(.*?)(:.*?)?/.*$|$1|')"
30-
31-
echo "Using custom PyPI index: $UV_INDEX_URL"
32-
echo "Insecure host: $UV_INSECURE_HOST"
34+
if command -v uv &>/dev/null; then
35+
export UV_INDEX_URL="$PYPI_INDEX_URL"
36+
export UV_INSECURE_HOST="$(echo $PYPI_URL | perl -pe 's|^.*?://(.*?)(:.*?)?/.*$|$1|')"
37+
echo "Using custom PyPI index (uv): $UV_INDEX_URL"
38+
else
39+
mkdir -p /root/.pip
40+
echo "[global]" > /root/.pip/pip.conf
41+
echo "index = $PYPI_URL" >> /root/.pip/pip.conf
42+
echo "index-url = $PYPI_INDEX_URL" >> /root/.pip/pip.conf
43+
echo "trusted-host = $(echo $PYPI_URL | perl -pe 's|^.*?://(.*?)(:.*?)?/.*$|$1|')" >> /root/.pip/pip.conf
44+
echo "Using custom pip.conf:"
45+
cat /root/.pip/pip.conf
46+
fi
3347
fi
3448

3549
cd $WORKDIR
3650

3751
if [ -f requirements.txt ]; then
38-
uv pip install --system -r requirements.txt
52+
$PIP_INSTALL -r requirements.txt
3953
fi # [ -f requirements.txt ]
4054

4155
echo "$@"

test/main.py

Lines changed: 29 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,27 @@
1-
import logging
2-
import platform # New import for OS information
3-
import time
1+
import platform
2+
import sys
43

54
import numpy as np
65
import pandas as pd
7-
import requests
86
from flask import Flask, jsonify
97
from flask_wtf.csrf import CSRFProtect
108
from sqlalchemy import create_engine, text
119

12-
# Logging configuration
13-
logging.basicConfig(
14-
filename="app.log",
15-
filemode="a",
16-
level=logging.INFO,
17-
format="%(asctime)s - %(levelname)s - %(message)s",
18-
)
1910

11+
def check_platform():
12+
info = platform.platform()
13+
print(f"Platform: {info}")
2014

21-
# Function to log OS information
22-
def log_os_info():
23-
os_info = platform.platform()
24-
logging.info(f"Operating System: {os_info}")
2515

26-
27-
# Function to check requests functionality
28-
def check_requests():
29-
max_retries = 3
30-
for attempt in range(1, max_retries + 1):
31-
try:
32-
response = requests.get("https://api.github.com")
33-
response.raise_for_status()
34-
logging.info(f"Request succeeded with status {response.status_code}")
35-
print(f"Response Content: {response.json()}")
36-
return
37-
except requests.exceptions.HTTPError as http_err:
38-
logging.error(f"HTTP error on attempt {attempt}: {http_err}")
39-
except Exception as err:
40-
logging.error(f"Other error on attempt {attempt}: {err}")
41-
time.sleep(2)
42-
print("All retries failed.")
43-
44-
45-
# Function to test Pandas and NumPy functionality
46-
def data_processing():
16+
def check_numpy_pandas():
4717
data = np.random.rand(5, 3)
4818
df = pd.DataFrame(data, columns=["A", "B", "C"])
49-
logging.info(f"Generated DataFrame:\n{df}")
50-
print(df.describe()) # Data statistics
19+
assert df.shape == (5, 3), f"Expected shape (5, 3), got {df.shape}"
20+
assert list(df.columns) == ["A", "B", "C"]
21+
print(f"NumPy/Pandas OK: {df.shape}")
5122

5223

53-
# Function to test Flask functionality
54-
def start_flask_app():
24+
def check_flask():
5525
app = Flask(__name__)
5626
csrf = CSRFProtect()
5727
csrf.init_app(app)
@@ -60,31 +30,33 @@ def start_flask_app():
6030
def index():
6131
return jsonify(message="Hello from Flask!")
6232

63-
app.run(port=5000)
33+
client = app.test_client()
34+
response = client.get("/")
35+
assert response.status_code == 200, f"Flask returned {response.status_code}"
36+
print("Flask OK")
6437

6538

66-
# Function to test SQLAlchemy functionality
67-
def check_database():
39+
def check_sqlalchemy():
6840
engine = create_engine("sqlite:///:memory:")
6941
with engine.connect() as connection:
7042
result = connection.execute(text("SELECT 'Hello, SQLAlchemy!'"))
71-
for row in result:
72-
logging.info(f"SQLAlchemy result: {row[0]}")
73-
print(f"SQLAlchemy result: {row[0]}")
43+
row = result.fetchone()
44+
assert row is not None, "SQLAlchemy returned no rows"
45+
assert row[0] == "Hello, SQLAlchemy!"
46+
print("SQLAlchemy OK")
7447

7548

76-
# Main function
7749
def main():
78-
log_os_info() # Log OS information
79-
logging.info("Starting main test process")
80-
check_requests()
81-
data_processing()
82-
check_database()
83-
# Run the Flask app in a separate thread if needed
84-
# from threading import Thread
85-
# flask_thread = Thread(target=start_flask_app)
86-
# flask_thread.start()
50+
check_platform()
51+
check_numpy_pandas()
52+
check_flask()
53+
check_sqlalchemy()
54+
print("All checks passed")
8755

8856

8957
if __name__ == "__main__":
90-
main()
58+
try:
59+
main()
60+
except Exception as e:
61+
print(f"FATAL: {e}", file=sys.stderr)
62+
sys.exit(1)

0 commit comments

Comments
 (0)