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
103 changes: 103 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Test and Release

on:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/release.yml
printf '%s\n' '--- permission-related workflow entries ---'
rg -n -C 3 'permissions:|actions/checkout|uv sync|uv run|pull_request|push:' .github/workflows/release.yml

Repository: deployml-core/deployml

Length of output: 5901


Security Misconfiguration (CWE-269): Improper Privilege Management

Reachability: External

Set explicit baseline token permissions.

Add workflow-level permissions: { contents: read } for the lint and test jobs. Keep the existing release-job override for publishing and release commits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 3, Add workflow-level permissions
granting only contents: read for the lint and test jobs in the release workflow.
Preserve the existing release-job permissions override so publishing and release
commits retain their required access.

Source: Linters/SAST tools

push:
branches: [ main ]
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
branches: [ main ]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: both triggers here (push and pull_request) are scoped to branches: [ main ] only. Is folding dev in (e.g. branches: [main, dev] on the pull_request trigger) out of scope for this migration, or intentionally deferred? As-is, PRs into dev still get zero CI from this workflow — same gap the old test-pypi.yml had.

paths-ignore:
- 'docs/**'
- '*.md'
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
file=".github/workflows/release.yml"
printf '%s\n' '--- workflow context ---'
nl -ba "$file" | sed -n '1,95p'
printf '%s\n' '--- checkout and pull_request occurrences ---'
rg -n -C 3 'pull_request|actions/checkout|uv sync|uv run pytest|persist-credentials|GITHUB_TOKEN|semantic-release' "$file"

Repository: deployml-core/deployml

Length of output: 2495


🏁 Script executed:

#!/bin/bash
set -eu
file=".github/workflows/release.yml"
printf '%s\n' '--- workflow ---'
sed -n '1,110p' "$file"
printf '%s\n' '--- repository references to this workflow ---'
rg -n -C 2 'release\.yml|pull_request|persist-credentials' .github README.md 2>/dev/null || true
printf '%s\n' '--- bounded workflow verifier ---'
python3 - <<'PY'
from pathlib import Path

p = Path(".github/workflows/release.yml")
lines = p.read_text().splitlines()

event = None
job = None
checkouts = []
commands = []

for i, raw in enumerate(lines, 1):
    s = raw.strip()
    if s == "pull_request:":
        event = "pull_request"
    if raw.startswith("  ") and not raw.startswith("    ") and s.endswith(":"):
        job = s[:-1]
    if s.startswith("- uses: actions/checkout@"):
        with_block = []
        for nxt in lines[i:i+5]:
            if nxt.startswith("        ") or nxt.strip() == "":
                with_block.append(nxt.strip())
            else:
                break
        checkouts.append((i, job, event, with_block))
    if s.startswith("run:"):
        commands.append((i, job, event, s[4:].strip()))

print({"pull_request_event_seen": event == "pull_request"})
print({"checkouts": checkouts})
print({"commands": [x for x in commands if x[1] in {"lint", "test", "release"}]})
PY

Repository: deployml-core/deployml

Length of output: 3946


Sensitive Data Exposure (CWE-522): Insufficiently Protected Credentials

Reachability: External · Exploitability: Trivial

Disable credential persistence in pull-request jobs.

Add persist-credentials: false to the actions/checkout@v4 steps at lines 20 and 41. Pull-request code runs uv sync and uv run pytest, so it can read the persisted GITHUB_TOKEN from Git configuration. Keep credential persistence in the release job because semantic-release pushes commits and tags.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 20-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

📍 Affects 1 file
  • .github/workflows/release.yml#L20-L20 (this comment)
  • .github/workflows/release.yml#L41-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 20, Update the actions/checkout@v4
steps at .github/workflows/release.yml lines 20-20 and 41-41 to set
persist-credentials to false for both pull-request jobs. Leave the release job’s
checkout configuration unchanged so semantic-release can continue pushing
commits and tags.

Source: Linters/SAST tools


- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
enable-cache: true

- name: Sync dependencies
run: uv sync
Comment on lines +28 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- workflow ---'
cat -n .github/workflows/release.yml | sed -n '1,105p'

printf '%s\n' '--- uv configuration and lock usage ---'
rg -n --hidden -g '!**/.git/**' 'uv (sync|lock)|locked|frozen|uv\.lock|PACKAGE_NAME' .

Repository: deployml-core/deployml

Length of output: 5201


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- pyproject release configuration ---'
cat -n pyproject.toml | sed -n '1,95p'

printf '%s\n' '--- lockfile and project metadata ---'
ls -l uv.lock pyproject.toml
sed -n '1,45p' uv.lock

Repository: deployml-core/deployml

Length of output: 5825


🌐 Web query:

uv sync --locked documentation behavior stale lockfile

💡 Result:

When you use the --locked flag with uv sync, it instructs uv to assert that the existing uv.lock file is up-to-date with your current project configuration (such as pyproject.toml) [1][2]. If the lockfile is missing or is not up-to-date—meaning your project's dependency constraints have changed in a way that requires the lockfile to be regenerated—uv will exit with an error instead of attempting to update the lockfile [1][2][3]. This behavior is designed to ensure that the environment is strictly reproducible and to prevent unintended modifications to the lockfile in environments where that is restricted, such as CI/CD pipelines or production deployments [1][2]. In summary: * Behavior: uv verifies the lockfile against current project metadata [1][4]. * Stale Lockfile: If the lockfile is stale (out of sync with dependencies), the command fails with an error [1][3]. * Use Case: This is best for environments where you want to ensure the installation matches the existing lockfile exactly and fail if it does not [2][4]. If you intend to bypass the requirement that the lockfile must match the project's dependency constraints and simply want to install based on the existing lockfile without modification, you should use --frozen instead [2][4][3]. While --locked validates the lockfile's integrity, --frozen treats the lockfile as the immutable source of truth and ignores any potential drifts in the project's dependency definitions [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

workflow = Path(".github/workflows/release.yml").read_text()
pyproject = Path("pyproject.toml").read_text()

sync_runs = [line.strip() for line in workflow.splitlines() if line.strip() == "run: uv sync"]
print("plain uv sync steps:", len(sync_runs))
print("all expected sync sites present:", len(sync_runs) == 3)
print('semantic-release retains uv lock upgrade:', 'uv lock --upgrade-package "$PACKAGE_NAME"' in pyproject)
PY

if command -v uv >/dev/null 2>&1; then
  uv --version
  uv sync --help | grep -E -- '--locked|--frozen' || true
else
  echo "uv is not available in the verifier sandbox"
fi

Repository: deployml-core/deployml

Length of output: 312


Make CI reject a stale uv.lock.

Use uv sync --locked at all three dependency-sync steps. Keep uv lock --upgrade-package "$PACKAGE_NAME" in the semantic-release build command because that path intentionally updates uv.lock.

📍 Affects 1 file
  • .github/workflows/release.yml#L28-L29 (this comment)
  • .github/workflows/release.yml#L49-L50
  • .github/workflows/release.yml#L76-L77
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 28 - 29, Update all three
dependency-sync steps in .github/workflows/release.yml at lines 28-29, 49-50,
and 76-77 to run the locked sync mode so CI rejects a stale uv.lock. Leave the
semantic-release build command’s uv lock --upgrade-package "$PACKAGE_NAME"
invocation unchanged because it intentionally updates the lockfile.


# Check only, never fix. Contributors auto-fix locally via the pre-commit hook.
- name: Ruff lint
run: uv run ruff check .

- name: Ruff format
run: uv run ruff format --check .

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
enable-cache: true

- name: Sync dependencies
run: uv sync

- name: Run tests
run: uv run pytest
Comment on lines +52 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/release.yml | sed -n '1,120p'
printf '%s\n' '--- project files ---'
git ls-files | sed -n '1,160p'
printf '%s\n' '--- packaging and test configuration ---'
for f in pyproject.toml setup.cfg setup.py tox.ini noxfile.py; do
  if [ -f "$f" ]; then
    printf '%s\n' "--- $f ---"
    cat -n "$f" | sed -n '1,220p'
  fi
done
printf '%s\n' '--- smoke test ---'
if [ -f tests/test_smoke.py ]; then
  cat -n tests/test_smoke.py | sed -n '1,220p'
fi

Repository: deployml-core/deployml

Length of output: 14974


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

workflow = Path(".github/workflows/release.yml").read_text()
pyproject = Path("pyproject.toml").read_text()
tests = sorted(str(p) for p in Path("tests").glob("test_*.py"))

print("smoke_test_present:", Path("tests/test_smoke.py").is_file())
print("test_files:", tests)
print("test_job_syncs:", bool(re.search(r"(?ms)^  test:.*?^\s+run: uv sync$", workflow)))
print("test_job_runs_pytest:", bool(re.search(r"(?ms)^  test:.*?^\s+run: uv run pytest$", workflow)))
print("release_builds_wheel:", "uv build" in pyproject)
print("release_build_command:")
match = re.search(r'(?ms)^\s*build_command\s*=\s*"""\n(.*?)\n\s*"""', pyproject)
print(match.group(1) if match else "<not found>")
PY
printf '%s\n' '--- repository status ---'
git status --short
printf '%s\n' '--- test references ---'
rg -n 'test_smoke|uv sync|uv build|semantic-release|editable' .github pyproject.toml tests README.md docs || true

Repository: deployml-core/deployml

Length of output: 1631


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- smoke test and related tests ---'
cat -n tests/test_smoke.py | sed -n '1,140p'
printf '%s\n' '--- lockfile project entry ---'
rg -n -A18 -B4 'name = "deployml-core"|source = \{ editable' uv.lock || true
printf '%s\n' '--- relevant packaging docs ---'
cat -n docs/contributing.md | sed -n '1,60p'
printf '%s\n' '--- uv availability ---'
if command -v uv >/dev/null 2>&1; then
  uv --version
else
  printf '%s\n' 'uv unavailable in sandbox'
fi

Repository: deployml-core/deployml

Length of output: 5794


Test the built wheel before release.

uv sync installs the project in editable mode, so the smoke tests do not validate the wheel. Build and install the wheel in a clean environment, then run tests/test_smoke.py before publishing.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 38-53: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 52 - 53, Update the release
workflow’s “Run tests” step to build the wheel, install that wheel in a clean
environment, and execute tests/test_smoke.py against the installed artifact
before publishing. Do not rely on the editable installation created by uv sync.


release:
needs: [lint, test]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: write # semantic-release pushes the version commit + tag + GitHub Release
id-token: write # OIDC trusted publishing to PyPI (no token needed)
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # semantic-release needs full history to analyse commits

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
enable-cache: true

- name: Sync dependencies
run: uv sync

- name: Python Semantic Release (bump, changelog, tag, build)
id: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
rm -rf dist
uv run semantic-release version
if ls dist/*.whl >/dev/null 2>&1; then
echo "released=true" >> "$GITHUB_OUTPUT"
else
echo "released=false" >> "$GITHUB_OUTPUT"
echo "No release-worthy commits since the last tag; skipping publish."
fi

- name: Publish to PyPI (trusted publishing)
if: steps.release.outputs.released == 'true'
run: uv publish

- name: Attach artifacts to the GitHub Release
if: steps.release.outputs.released == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: uv run semantic-release publish
135 changes: 0 additions & 135 deletions .github/workflows/test-pypi.yml

This file was deleted.

1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
__pycache__
poetry.lock
.DS_Store
.terraform
.terraform.lock.hcl
Expand Down
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Install with: uv run pre-commit install
# Installs both the pre-commit (ruff) and commit-msg (commitizen) hooks.
default_install_hook_types: [pre-commit, commit-msg]
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. Keep in sync with the dev-group ruff pin in pyproject.toml.
rev: v0.15.21
hooks:
# Run the linter.
- id: ruff-check
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.10.1
hooks:
# Validate Conventional Commit messages (semantic-release parses these).
- id: commitizen
stages: [commit-msg]
26 changes: 26 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ You can help improve the project in several ways:
2. Follow the setup instructions in the main README to install dependencies and configure your environment.
3. Create a new branch for your work with a descriptive name (e.g., fix-deployment-logging or add-example-yaml).

## Development

This project uses [uv](https://docs.astral.sh/uv/) for dependency management, building, and publishing.

- Install dependencies (including the `dev` group): `uv sync`
- Set up the git hooks: `uv run pre-commit install` (wires up both the ruff and
commit-message hooks in one step)
- Run the tests: `uv run pytest`
- Build the package locally: `uv build`

[ruff](https://docs.astral.sh/ruff/) runs on every commit and auto-fixes lint and
formatting locally. CI runs the same checks in check-only mode, so a PR won't merge
if anything is left unformatted. The commit-message hook rejects commits that don't
follow the Conventional Commits format below.

## Releases and Commit Messages

Releases are fully automated with [python-semantic-release](https://python-semantic-release.readthedocs.io/). The next version, changelog, git tag, GitHub Release, and PyPI upload are all derived from commit messages on `main`, so **do not bump the version by hand**.

Use [Conventional Commits](https://www.conventionalcommits.org/) so the version bump is correct:

- `fix:` → patch release (e.g. `0.1.0` → `0.1.1`)
- `feat:` → minor release (e.g. `0.1.0` → `0.2.0`)
- `feat!:` or a `BREAKING CHANGE:` footer → major release
- `docs:`, `chore:`, `refactor:`, `test:`, etc. → no release on their own

## Submitting a Pull Request

1. Push your changes to your fork.
Expand Down
18 changes: 9 additions & 9 deletions example/scripts/01_load_training_data.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
Step 1: Generate synthetic housing data and load into BigQuery offline_features table.
"""

import os
import uuid
import numpy as np
import pandas as pd
from datetime import datetime, timezone
from pathlib import Path
from dotenv import load_dotenv
Expand All @@ -27,16 +27,16 @@

np.random.seed(RANDOM_SEED)

cities = list(range(5)) # 0-4 representing 5 cities
states = list(range(3)) # 0-2 representing 3 states
cities = list(range(5)) # 0-4 representing 5 cities
states = list(range(3)) # 0-2 representing 3 states

bedrooms = np.random.randint(1, 6, N_ROWS).astype(float)
bathrooms = np.random.randint(1, 4, N_ROWS).astype(float)
area_sqft = np.random.randint(800, 4000, N_ROWS).astype(float)
lot_size = np.random.randint(2000, 10000, N_ROWS).astype(float)
bedrooms = np.random.randint(1, 6, N_ROWS).astype(float)
bathrooms = np.random.randint(1, 4, N_ROWS).astype(float)
area_sqft = np.random.randint(800, 4000, N_ROWS).astype(float)
lot_size = np.random.randint(2000, 10000, N_ROWS).astype(float)
year_built = np.random.randint(1960, 2023, N_ROWS).astype(float)
city = np.random.choice(cities, N_ROWS).astype(float)
state = np.random.choice(states, N_ROWS).astype(float)
city = np.random.choice(cities, N_ROWS).astype(float)
state = np.random.choice(states, N_ROWS).astype(float)

now = datetime.now(timezone.utc)
rows = [
Expand Down
Loading
Loading