Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Auto-merge (dependabot-auto-merge.yml) supplies the ruleset's guarded bot approval
# and squash-merges after Validate is green. Human-authored PRs still require human
# approval. auto-fix.yml mechanically repairs lint-only failures.
# Non-releasing commit prefixes (chore/ci) so a merged bump never cuts a release.
# chore/ci do not request a bump themselves; accumulated history can still release.
version: 2

updates:
Expand Down
17 changes: 16 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,26 @@ Commit title, e.g.:

- `feat: add X` (minor release)
- `fix: correct Y` (patch release)
- `ci:` / `chore:` / `docs:` / `refactor:` / `test:` (no release)
- `ci:` / `chore:` / `docs:` / `refactor:` / `test:` (no bump by themselves)

## Checklist

- [ ] The PR title is a valid Conventional Commit.
- [ ] The local gate passes: `uv run prek run --all-files` (see `AGENTS.md`).
- [ ] No changes to `src/skill_optimizer/**`, `optimize_description_v2.py`, or
`tests/**` unless intended.

## Dependency release impact

For dependency, bootstrap or packaging updates, complete the
[release-impact check](../docs/dependency-release-impact.md):

- Changed dependency and advisory/fixed-version evidence:
- Affected surface: runtime/bundled, source build, or CI/development/bootstrap:
- Published version/tag and actual wheel/source-archive evidence compared:
- Candidate build/install evidence:
- Accumulated commits and read-only semantic-release decision:
- Release decision: no distribution needed / new distribution needed / blocked:
- If a release would be triggered by merge, approval and expected contents:

For other changes, state why this section is not applicable.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version-file: uv.lock
# No cache in the publish path — avoids a cache-poisoning vector where a
# restored cache could influence the artifact that gets published.
enable-cache: false
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version-file: uv.lock

- name: Sync the environment
env:
Expand Down Expand Up @@ -68,7 +68,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version-file: uv.lock
# gate owns the shared cache write; parallel saves race on the same key.
save-cache: false

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version-file: uv.lock

# --locked asserts uv.lock is already in sync with pyproject.toml and fails
# otherwise, so a dependency change that skips `uv lock` cannot merge. The gate
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version-file: uv.lock
# gate owns the shared cache write; parallel saves race on the same key.
save-cache: false

Expand Down Expand Up @@ -179,7 +179,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version-file: uv.lock

- name: Compute mechanical fixes as self-contained data
id: patch
Expand Down
19 changes: 17 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ so `uv sync` installs it). Install the git hooks once with `prek install -f`.

- **Conventional Commits.** PRs are squash-merged, so the **PR title becomes the
commit message** that drives releases. Use `feat:` (minor), `fix:` (patch), or a
non-releasing type (`ci:`, `chore:`, `docs:`, `refactor:`, `test:`). The `pr-title`
check enforces this.
type that does not itself request a bump (`ci:`, `chore:`, `docs:`, `refactor:`,
`test:`). Earlier unreleased changes can still trigger a release. The `pr-title`
check enforces the title syntax.
- **Keep the tool code stable.** `src/skill_optimizer/**`, `optimize_description_v2.py`,
and `tests/**` are the finished tool; change them only with intent.

Expand Down Expand Up @@ -123,3 +124,17 @@ bash scripts/audit-dependencies.sh
Reports are written under `${RUNNER_TEMP:-${TMPDIR:-/tmp}}/optimizer-dependency-audit`.
Use the raw reports to investigate a failure; never change a scanner exit code or
weaken a required check to make an update merge.

## Dependency changes and published artifacts

Complete the [dependency release-impact check](docs/dependency-release-impact.md)
for dependency, bootstrap and packaging PRs. Green checks on main do not prove that
an already published source archive or wheel contains an update. Record whether a
new distribution is needed and what the existing main-push release workflow would
do before merging.

The uv executable has one exact, Dependabot-owned requirement in the non-default
`bootstrap` group. Its `uv.lock` entry supplies every setup-uv step and the semantic
release build hook. Both scanners cover it, and the audit refuses to run with a
different installed uv version. Update the requirement and lock together, without
adding a separate workflow or hook version override.
74 changes: 74 additions & 0 deletions docs/dependency-release-impact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Dependency release-impact check

A green dependency scan on `main` is not proof that published artifacts contain a
change. Classify every dependency PR before merge and record its release decision
in the PR. The pull-request template provides the checklist. Do not change release
permissions, publish flags, or security exceptions to make a check pass.

## What needs a release?

- **Runtime or bundled dependency:** inspect the published wheel's `METADATA`
(`Requires-Dist`) and archive contents. If affected code or a dependency
requirement ships to users, a new distribution is needed to deliver the fix.
- **Build backend or source-install requirement:** inspect the published source
archive's `pyproject.toml`. A changed build-system range reaches source-install
users only in a new source distribution. This is distinct from a vulnerability
in the installed runtime.
- **CI, development, or release bootstrap only:** a workflow/lock update protects
future development and build runs after merge. A new runtime release is not
needed solely to change the maintainer's executable. Confirm that it is not a
runtime dependency or bundled into the wheel.
- **Mixed change:** document each surface separately. Do not infer release impact
from a package name, dependency group, or the PR title alone.

## Evidence to record before merge

1. Record the exact PR head SHA, current `main` SHA, latest release tag and its
commit, and the published version/files being compared. Compare all commits
since that release, including their bodies and breaking-change markers.
2. Record exact old/new dependency versions, advisory/fixed-version evidence, and
the lockfile inventory used by the unsuppressed scanners. For uv, verify the
installed executable against the lock with
`uv run --locked python -m scripts.bootstrap_uv --installed-version "$(uv --version)"`.
3. Inspect the actual published wheel and source archive. Record runtime
requirements, build requirements, and any bundled changed code. If published
files are unavailable, mark this comparison blocked rather than infer parity
from repository metadata.
4. Build a candidate with `uv build --no-sources`, inspect its wheel `METADATA` and
source `pyproject.toml`, and exercise the documented install/entry-point checks.
A local build is verification, not publication. Follow `AGENTS.md`'s live-CLI
release gates before approving a release.
5. Check the pinned python-semantic-release version's **read-only**
`semantic-release version --print` result against the proposed history. Also
inspect the release workflow's triggers and publish condition. `ci:` and
`chore:` do not themselves request a bump, but an earlier unreleased `fix:` or
`feat:` can cause this merge to release accumulated changes.
6. State one decision: **no distribution needed**, **new distribution needed**, or
**blocked pending evidence/approval**. State what a release would contain and
whether the existing main-push workflow will create one. If publication needs
approval, obtain it before merging a change that would trigger that publication.
7. After an approved merge, verify checks on the exact main SHA and inspect the
Release run. If a release was approved, verify its tag/commit, published file
hashes and installed version. A tag or green validation alone does not prove
that PyPI accepted the expected wheel and source archive.

## Bootstrap remediation: 2026-10-21

[GHSA-hq85-5cpj-mr62](https://github.com/astral-sh/uv/security/advisories/GHSA-hq85-5cpj-mr62)
affects uv versions before 0.13.4. The repository's hardcoded 0.11.29 executable
was outside `uv.lock`, so lock-only vulnerability scans did not cover it.

The fix places `uv==0.13.4` in the non-default `bootstrap` dependency group and its
exact PyPI package in `uv.lock`. Every setup-uv step reads that lockfile; the
semantic-release hook reads and validates the same requirement. The audit verifies
the running executable before both scanners, and their existing full-inventory
checks now include uv. Missing pins, drift, incomplete scans and scanner errors
fail closed. The advisory-exception policy remains unchanged and empty.

This repairs build/CI tooling, not a bundled runtime dependency. The tool has no
third-party runtime requirements. Separately, the build backend range on main was
widened from `uv_build>=0.11.28,<0.13` in release v0.2.6 to
`uv_build>=0.11.28,<0.14`; source-install users need a later source distribution to
receive that compatibility change. A new runtime vulnerability was not established
by that range change. Record the final release decision and artifact evidence in
the remediation PR rather than assuming this documentation publishes anything.
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ module-name = "skill_optimizer"
module-root = "src"

[dependency-groups]
# Dependabot owns this exact bootstrap pin; setup-uv and semantic release read
# the matching uv.lock entry. Keep bootstrap out of the default dev environment.
bootstrap = ["uv==0.13.4"]
dev = [
"prek>=0.4.10",
"pydoclint>=0.9.1",
Expand Down Expand Up @@ -169,12 +172,15 @@ extraPaths = ["src"]
# GitHub Release. Building/publishing to PyPI is handled by a separate job in
# .github/workflows/release.yml (uv build + gh-action-pypi-publish). The build hook
# only keeps uv.lock synchronized with PSR's version stamp before the release commit.
# The uv pin must match the release workflow. See CONTRIBUTING.md and AGENTS.md.
# The bootstrap pin is owned by the group above and resolved from uv.lock.
# See CONTRIBUTING.md and AGENTS.md.
[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
build_command = """
set -eu
python -m pip install --disable-pip-version-check "uv==0.11.29"
UV_REQUIREMENT="$(python -m scripts.bootstrap_uv)"
python -m pip install --disable-pip-version-check "$UV_REQUIREMENT"
python -m scripts.bootstrap_uv --installed-version "$(uv --version)"
uv lock --upgrade-package "$PACKAGE_NAME"
git add uv.lock
"""
Expand Down
5 changes: 5 additions & 0 deletions scripts/audit-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ set -euo pipefail

output_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/optimizer-dependency-audit"
mkdir -p "$output_dir"
# Prove that the executable being used is the exact package included in both
# scanners' lock inventory. Missing or divergent bootstrap state fails closed.
uv run --locked python -m scripts.bootstrap_uv --installed-version "$(uv --version)" \
| tee "$output_dir/bootstrap.txt"

summary_args=()
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
summary_args=(--summary "$GITHUB_STEP_SUMMARY")
Expand Down
67 changes: 67 additions & 0 deletions scripts/bootstrap_uv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Bind the installed uv bootstrap executable to the audited lockfile package."""

import argparse
import re
import sys
import tomllib
from collections.abc import Sequence
from pathlib import Path

from scripts.advisory_policy import PolicyError, array, mapping, require, string


def bootstrap_requirement(project: object, lock: object) -> str:
"""Return the sole exact bootstrap requirement, rejecting divergent lock state."""
groups = mapping(mapping(project, "project").get("dependency-groups"), "groups")
requirements = array(groups.get("bootstrap"), "bootstrap group")
require(len(requirements) == 1, "bootstrap must contain exactly one uv requirement")
requirement = string(requirements[0], "bootstrap requirement")
require(
re.fullmatch(r"uv==[0-9]+\.[0-9]+\.[0-9]+", requirement) is not None,
"bootstrap must pin uv==X.Y.Z exactly",
)
packages = [
mapping(raw, "locked package")
for raw in array(mapping(lock, "lock").get("package"), "lock.package")
]
matches = [package for package in packages if package.get("name") == "uv"]
require(len(matches) == 1, "uv.lock must contain exactly one uv package")
package = matches[0]
require(
package.get("source") == {"registry": "https://pypi.org/simple"},
"bootstrap must be a PyPI registry package in the scanner inventory",
)
require(
requirement == f"uv=={string(package.get('version'), 'uv version')}",
"bootstrap requirement and uv.lock version disagree",
)
return requirement


def main(argv: Sequence[str] | None = None) -> int:
"""Print the locked pip requirement and optionally verify the running executable."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project", type=Path, default=Path("pyproject.toml"))
parser.add_argument("--lock", type=Path, default=Path("uv.lock"))
parser.add_argument("--installed-version")
args = parser.parse_args(argv)
try:
requirement = bootstrap_requirement(
tomllib.loads(args.project.read_text(encoding="utf-8")),
tomllib.loads(args.lock.read_text(encoding="utf-8")),
)
if args.installed_version is not None:
require(
args.installed_version.split()[:2]
== ["uv", requirement.removeprefix("uv==")],
"installed uv version differs from the audited bootstrap pin",
)
print(requirement)
except (PolicyError, ValueError, OSError) as error:
print(f"uv bootstrap check failed: {error}", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading