Slim CI to a single PR job and fix environment-dependent tests
- Consolidate workflow into one job (Python 3.11/3.13), drop pip-audit, package build, and best-effort integration tests - Make helper-ownership tests root-safe and the timefmt zone test deterministic across host timezones
This commit is contained in:
+12
-56
@@ -1,73 +1,29 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
precommit-and-security:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
|
|
||||||
- name: Install pre-commit
|
|
||||||
run: pip install pre-commit
|
|
||||||
|
|
||||||
- name: Run pre-commit hooks
|
|
||||||
run: pre-commit run --all-files --color always
|
|
||||||
|
|
||||||
- name: Install Poetry and export plugin
|
|
||||||
run: |
|
|
||||||
pip install poetry
|
|
||||||
poetry self add poetry-plugin-export
|
|
||||||
|
|
||||||
- name: Install pip-audit
|
|
||||||
run: pip install pip-audit
|
|
||||||
|
|
||||||
- name: Audit dev dependencies (Poetry lockfile)
|
|
||||||
run: |
|
|
||||||
poetry export -f requirements.txt --without-hashes --with dev \
|
|
||||||
| pip-audit -r /dev/stdin
|
|
||||||
|
|
||||||
typecheck:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
- run: pip install mypy
|
|
||||||
- run: mypy
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
python-version: ["3.11", "3.13"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- run: pip install -e . pytest
|
|
||||||
- run: pytest tests/unit tests/security
|
|
||||||
- name: Integration tests (best effort)
|
|
||||||
continue-on-error: true
|
|
||||||
run: pytest tests/integration
|
|
||||||
|
|
||||||
package:
|
- name: Install project and tooling
|
||||||
runs-on: ubuntu-latest
|
run: pip install -e . pytest mypy pre-commit
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
- name: Lint and typecheck
|
||||||
- uses: actions/setup-python@v5
|
if: matrix.python-version == '3.13'
|
||||||
with:
|
run: |
|
||||||
python-version: "3.12"
|
pre-commit run --all-files --color always
|
||||||
- run: pip install build
|
mypy
|
||||||
- run: python -m build
|
|
||||||
|
- name: Unit and security tests
|
||||||
|
run: pytest tests/unit tests/security
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ from schedls.security import (
|
|||||||
validate_name,
|
validate_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_UNTRUSTED_UID = 65534
|
||||||
|
|
||||||
|
|
||||||
|
def _relinquish_ownership(path) -> None:
|
||||||
|
"""Own a fixture by a non-root user when the suite itself runs as root.
|
||||||
|
|
||||||
|
The helper checks distinguish root-owned files from user-owned ones, so a
|
||||||
|
root test process must make its "untrusted" fixtures owned by someone else.
|
||||||
|
"""
|
||||||
|
if os.geteuid() == 0:
|
||||||
|
os.chown(path, _UNTRUSTED_UID, _UNTRUSTED_UID)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"name",
|
"name",
|
||||||
@@ -114,6 +126,7 @@ def test_resolve_helper_rejects_user_owned_dir_when_root(tmp_path, monkeypatch)
|
|||||||
helper = helper_dir / "evilhelper"
|
helper = helper_dir / "evilhelper"
|
||||||
helper.write_text("#!/bin/sh\ntrue\n")
|
helper.write_text("#!/bin/sh\ntrue\n")
|
||||||
helper.chmod(0o755)
|
helper.chmod(0o755)
|
||||||
|
_relinquish_ownership(helper_dir)
|
||||||
monkeypatch.setenv("PATH", str(helper_dir))
|
monkeypatch.setenv("PATH", str(helper_dir))
|
||||||
monkeypatch.setattr(os, "geteuid", lambda: 0)
|
monkeypatch.setattr(os, "geteuid", lambda: 0)
|
||||||
assert resolve_helper("evilhelper") is None
|
assert resolve_helper("evilhelper") is None
|
||||||
@@ -134,6 +147,7 @@ def test_runner_refuses_untrusted_absolute_helper(tmp_path, monkeypatch) -> None
|
|||||||
helper = tmp_path / "evil"
|
helper = tmp_path / "evil"
|
||||||
helper.write_text("#!/bin/sh\ntrue\n")
|
helper.write_text("#!/bin/sh\ntrue\n")
|
||||||
helper.chmod(0o755)
|
helper.chmod(0o755)
|
||||||
|
_relinquish_ownership(helper)
|
||||||
monkeypatch.setattr(os, "geteuid", lambda: 0)
|
monkeypatch.setattr(os, "geteuid", lambda: 0)
|
||||||
with pytest.raises(SafetyRefusalError):
|
with pytest.raises(SafetyRefusalError):
|
||||||
CommandRunner().run([str(helper)])
|
CommandRunner().run([str(helper)])
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
from datetime import UTC, datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -7,6 +8,15 @@ import pytest
|
|||||||
from schedls import timefmt
|
from schedls import timefmt
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def london_tz(monkeypatch):
|
||||||
|
monkeypatch.setenv("TZ", "Europe/London")
|
||||||
|
time.tzset()
|
||||||
|
yield
|
||||||
|
monkeypatch.undo()
|
||||||
|
time.tzset()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("value", "seconds"),
|
("value", "seconds"),
|
||||||
[
|
[
|
||||||
@@ -50,10 +60,11 @@ def test_parse_placeholders() -> None:
|
|||||||
assert timefmt.parse_systemd_timestamp("0") is None
|
assert timefmt.parse_systemd_timestamp("0") is None
|
||||||
|
|
||||||
|
|
||||||
def test_format_datetime_round_trip_zone() -> None:
|
def test_format_datetime_round_trip_zone(london_tz) -> None:
|
||||||
dt = datetime(2026, 9, 25, 2, 0, 0, tzinfo=timezone(timedelta(hours=1)))
|
dt = datetime(2026, 9, 25, 2, 0, 0, tzinfo=timezone(timedelta(hours=1)))
|
||||||
text = timefmt.format_datetime(dt)
|
text = timefmt.format_datetime(dt)
|
||||||
assert text.startswith("Fri 25 Sep 2026 02:00:00")
|
assert text.startswith("Fri 25 Sep 2026 02:00:00")
|
||||||
|
assert text.endswith("BST")
|
||||||
|
|
||||||
|
|
||||||
def test_format_short_relative() -> None:
|
def test_format_short_relative() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user