-
Notifications
You must be signed in to change notification settings - Fork 5.1k
ci: add security-aware Dependabot updates for Python and GitHub Actions #3641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
20c1c08
4688d3b
42e7f73
9eacb6d
05d03fc
253692a
86b9b62
6af5be9
69acefe
70d1ffa
3e188b8
0b3f84b
d5e8f30
05d1c29
2a4d89e
dd2e769
3860753
27b0b83
2b16120
4d25ed8
f1f729e
eb134b8
9071f2b
05c414f
f012570
1ef8c69
a0049ec
7a3b31a
95769f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,11 +18,58 @@ on: | |
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| UV_NO_BUILD: '1' | ||
|
HAYDEN-OAI marked this conversation as resolved.
|
||
| UV_NO_BINARY_PACKAGE: 'openai' | ||
| UNTRUSTED_BUILD_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && '1' || '0' }} | ||
| TRUSTED_BUILD_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }} | ||
|
|
||
| jobs: | ||
| dependency-locks: | ||
|
HAYDEN-OAI marked this conversation as resolved.
|
||
| timeout-minutes: 10 | ||
| name: dependency lock freshness | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Verify dependency source provenance before installing tools | ||
| run: | | ||
| python -I -c 'import ast, os, pathlib, re, subprocess, tomllib; config = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); fork = os.environ.get("UNTRUSTED_BUILD_FORK", "0") == "1"; base = os.environ.get("TRUSTED_BUILD_BASE_SHA", ""); assert not fork or re.fullmatch(r"[0-9a-f]{40}", base), "Untrusted fork build base commit"; assert not fork or subprocess.check_output(["git", "remote", "get-url", "origin"], text=True).strip() in {"https://github.com/openai/openai-python", "https://github.com/openai/openai-python.git"}, "Untrusted fork build base origin"; subprocess.run(["git", "fetch", "--no-tags", "--depth=1", "origin", base], check=True) if fork else None; trusted = tomllib.loads(subprocess.check_output(["git", "show", base + ":pyproject.toml"], text=True)) if fork else config; project = config["project"]; lock = tomllib.loads(pathlib.Path("uv.lock").read_text()); trusted_lock = tomllib.loads(subprocess.check_output(["git", "show", base + ":uv.lock"], text=True)) if fork else lock; roots = [package for package in lock["package"] if package.get("name") == project["name"] and package.get("version") == project["version"] and package.get("source") == {"editable": "."}]; build = config.get("build-system", {}); uv = config.get("tool", {}).get("uv", {}); requires = build.get("requires", []); constraints = uv.get("build-constraint-dependencies", []); reviewed = config.get("dependency-groups", {}).get("build", []); canonical = lambda value: re.sub(r"[-_.]+", "-", value).lower(); package_identity = lambda package: (canonical(package["name"]), package["version"], package.get("source", {}).get("registry", ""), tuple(sorted((artifact["url"], artifact["hash"]) for artifact in (([package["sdist"]] if "sdist" in package else []) + package.get("wheels", []))))); normalize = lambda value: re.sub(r"\s+", "", value); identity = lambda value: (canonical((item := re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s*==\s*([0-9]+(?:\.[0-9]+)*(?:\.post[0-9]+)?)(?:\s*;\s*(.+))?", value)).group(1)), item.group(2), ast.dump(ast.parse(item.group(3), mode="eval")) if item.group(3) is not None else ""); assert len(roots) == 1 and canonical(project["name"]) == "openai" and sum(canonical(package.get("name", "")) == "openai" for package in lock["package"]) == 1 and all(package is roots[0] or package.get("source") == {"registry": "https://pypi.org/simple"} and isinstance(package.get("wheels"), list) and bool(package.get("wheels")) and (artifacts := ([package["sdist"]] if "sdist" in package else []) + package.get("wheels", [])) and all(isinstance(artifact, dict) and re.fullmatch(r"https://files\.pythonhosted\.org/packages/[^\s?#]+", artifact.get("url", "")) and re.fullmatch(r"sha256:[0-9a-f]{64}", artifact.get("hash", "")) for artifact in artifacts) for package in lock["package"]) and (not fork or sorted(package_identity(package) for package in lock["package"] if package is not roots[0]) == sorted(package_identity(package) for package in trusted_lock["package"] if package.get("source") != {"editable": "."})) and build.get("build-backend") == "hatchling.build" and "backend-path" not in build and isinstance(requires, list) and bool(requires) and isinstance(constraints, list) and bool(constraints) and isinstance(reviewed, list) and bool(reviewed) and not any(uv.get(key) for key in ("sources", "index", "indexes", "index-url", "extra-index-url", "default-index", "find-links", "index_url", "extra_index_url", "default_index", "find_links", "no-binary", "no-binary-package", "no_binary", "no_binary_package")) and not any(pathlib.Path(path).exists() for path in ("uv.toml", ".uv.toml")) and all(isinstance(requirement, str) and (match := re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s*==\s*([0-9]+(?:\.[0-9]+)*(?:\.post[0-9]+)?)(?:\s*;\s*(.+))?", requirement)) is not None and (match.group(3) is None or isinstance(marker := ast.parse(match.group(3), mode="eval").body, ast.Compare) and isinstance(marker.left, ast.Name) and len(marker.ops) == 1 and type(marker.ops[0]) in {ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn} and len(marker.comparators) == 1 and isinstance(marker.comparators[0], ast.Constant) and isinstance(marker.comparators[0].value, str)) and any(canonical(package.get("name", "")) == canonical(match.group(1)) and package.get("version") == match.group(2) and package.get("source") == {"registry": "https://pypi.org/simple"} for package in lock["package"]) for requirement in requires + constraints + reviewed) and {normalize(requirement) for requirement in constraints} == {normalize(requirement) for requirement in reviewed} and {normalize(requirement) for requirement in requires}.issubset({normalize(requirement) for requirement in constraints}) and any(canonical(requirement.split("==", 1)[0].strip()) == "hatchling" for requirement in requires) and build.get("build-backend") == trusted.get("build-system", {}).get("build-backend") and {identity(requirement) for requirement in requires} == {identity(requirement) for requirement in trusted.get("build-system", {}).get("requires", [])} and {identity(requirement) for requirement in constraints} == {identity(requirement) for requirement in trusted.get("tool", {}).get("uv", {}).get("build-constraint-dependencies", [])} and {identity(requirement) for requirement in reviewed} == {identity(requirement) for requirement in trusted.get("dependency-groups", {}).get("build", [])}, "Use only the public PyPI registry, reviewed artifacts, the exact editable root project, and locked reviewed build requirements in uv.lock"' | ||
|
|
||
| - name: Require published minimums for direct security updates | ||
| if: >- | ||
| github.event_name == 'pull_request' | ||
| && github.event.pull_request.user.login == 'dependabot[bot]' | ||
| && contains(github.event.pull_request.head.ref, 'python-security') | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| run: | | ||
| set -euo pipefail | ||
| [[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]] | ||
| origin="$(git remote get-url origin)" | ||
| [[ "$origin" == "https://github.com/openai/openai-python" || "$origin" == "https://github.com/openai/openai-python.git" ]] | ||
| git fetch --no-tags --depth=1 origin "$BASE_SHA" | ||
| git show "$BASE_SHA:scripts/check-dependency-security.py" | python -I - | ||
|
|
||
|
Comment on lines
+52
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fresh evidence beyond loading the checker from AGENTS.md reference: AGENTS.md:L21-L24 Useful? React with 👍 / 👎. |
||
| - name: Set up uv | ||
| uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | ||
| with: | ||
| version: '0.12.5' | ||
| enable-cache: true | ||
|
|
||
| - name: Verify dependency groups and committed lock | ||
| run: | | ||
| uv run --no-project --python '>=3.11' python -c 'import pathlib, tomllib; project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); assert project["dependency-groups"]["dev"] and "dev" in project["tool"]["uv"]["default-groups"], "Keep the uv development dependency group enabled"' | ||
| uv run --no-project --python '>=3.11' python -c 'import pathlib, tomllib; project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); text = pathlib.Path("uv.lock").read_text(); lock = tomllib.loads(text); version = project["project"]["version"]; assert next(package["version"] for package in lock["package"] if package["name"] == "openai") == version and "version = " + chr(34) + version + chr(34) + " # x-release-please-version" in text, "Preserve the uv.lock release-please marker and project version"' | ||
| uv lock --check | ||
|
|
||
| lint: | ||
| timeout-minutes: 10 | ||
| name: lint | ||
| runs-on: ubuntu-latest | ||
| needs: dependency-locks | ||
| if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | ||
|
|
@@ -44,6 +91,7 @@ jobs: | |
| run: ./scripts/lint | ||
|
|
||
| build: | ||
| needs: dependency-locks | ||
| if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork | ||
| timeout-minutes: 10 | ||
| name: build | ||
|
|
@@ -65,7 +113,9 @@ jobs: | |
| run: uv sync --locked --all-extras | ||
|
|
||
| - name: Run build | ||
| run: ./scripts/build | ||
| run: | | ||
| external_packages="$(python -c 'import importlib, re, sys; parser = importlib.import_module("tomllib" if sys.version_info >= (3, 11) else "tomli"); packages = parser.loads(open("uv.lock").read())["package"]; roots = [package for package in packages if package.get("source") == {"editable": "."}]; external = [package for package in packages if package not in roots]; assert len(roots) == 1 and roots[0].get("name") == "openai" and external and all(package.get("source") == {"registry": "https://pypi.org/simple"} and isinstance(package.get("name"), str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", package["name"]) for package in external), "Refuse malformed external build package restrictions"; denied = sorted({re.sub(r"[-_.]+", "-", package["name"]).lower() for package in external}); assert denied and "openai" not in denied, "Refuse incomplete external build package restrictions"; print(" ".join(denied))')" | ||
| env -u UV_NO_BUILD UV_NO_BUILD_PACKAGE="$external_packages" ./scripts/build | ||
|
|
||
| - name: Validate Python version metadata | ||
| run: uv run --locked --all-extras python scripts/utils/validate-python-version-wheel.py | ||
|
|
@@ -96,6 +146,7 @@ jobs: | |
| timeout-minutes: 15 | ||
| name: test (Python ${{ matrix.python-version }}) | ||
| runs-on: ubuntu-latest | ||
| needs: dependency-locks | ||
| if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork | ||
| strategy: | ||
| fail-fast: false | ||
|
|
@@ -132,6 +183,7 @@ jobs: | |
| timeout-minutes: 20 | ||
| name: test (HTTPX2) | ||
| runs-on: ubuntu-latest | ||
| needs: dependency-locks | ||
| if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | ||
|
|
@@ -167,6 +219,7 @@ jobs: | |
| name: examples | ||
| environment: ci | ||
| runs-on: ubuntu-latest | ||
| needs: dependency-locks | ||
| if: github.repository == 'openai/openai-python' && github.event_name == 'push' && github.ref == 'refs/heads/main' | ||
|
|
||
| steps: | ||
|
|
@@ -209,8 +262,11 @@ jobs: | |
| timeout-minutes: 20 | ||
| name: compatibility (Python ${{ matrix.python-version }}) | ||
| runs-on: ubuntu-latest | ||
| needs: dependency-locks | ||
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | ||
| continue-on-error: ${{ matrix.experimental }} | ||
| env: | ||
| UV_NO_BUILD: ${{ matrix.experimental && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '0' || '1' }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.