bigfix-relevance-debugger 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bigfix_relevance_debugger-0.1.0/.github/actions/install-uv/action.yml +61 -0
- bigfix_relevance_debugger-0.1.0/.github/dependabot.yml +16 -0
- bigfix_relevance_debugger-0.1.0/.github/workflows/pre-commit.yaml +112 -0
- bigfix_relevance_debugger-0.1.0/.github/workflows/tag_and_release.yaml +351 -0
- bigfix_relevance_debugger-0.1.0/.github/workflows/test.yaml +175 -0
- bigfix_relevance_debugger-0.1.0/.gitignore +220 -0
- bigfix_relevance_debugger-0.1.0/.pre-commit-config.yaml +78 -0
- bigfix_relevance_debugger-0.1.0/.python-version +1 -0
- bigfix_relevance_debugger-0.1.0/AGENTS.md +144 -0
- bigfix_relevance_debugger-0.1.0/CLAUDE.md +1 -0
- bigfix_relevance_debugger-0.1.0/LICENSE +21 -0
- bigfix_relevance_debugger-0.1.0/PKG-INFO +337 -0
- bigfix_relevance_debugger-0.1.0/README.md +308 -0
- bigfix_relevance_debugger-0.1.0/pyproject.toml +90 -0
- bigfix_relevance_debugger-0.1.0/pyrightconfig.json +15 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/__init__.py +15 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/__main__.py +6 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/analysis.py +284 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/app.py +1276 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/evaluation.py +1223 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/log_bridge.py +88 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/upstream/__init__.py +17 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/upstream/besapi.py +114 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/upstream/relevance_analyzer.py +146 -0
- bigfix_relevance_debugger-0.1.0/src/bigfix_relevance_debugger/upstream/remote_client_relevance.py +121 -0
- bigfix_relevance_debugger-0.1.0/tests/conftest.py +79 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/test_analysis.py +343 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/test_app.py +2516 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/test_evaluation.py +1976 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/test_log_bridge.py +169 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/test_repo_config.py +89 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/test_upstream_rules.py +57 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/fixtures/besapi/empty.xml +10 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/fixtures/besapi/error.xml +7 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/fixtures/besapi/plural.xml +14 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/fixtures/besapi/singular.xml +12 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/fixtures/besapi/tuples.xml +19 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/test_besapi.py +144 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/test_relevance_analyzer.py +114 -0
- bigfix_relevance_debugger-0.1.0/tests/unit/upstream/test_remote_client_relevance.py +170 -0
- bigfix_relevance_debugger-0.1.0/uv.lock +2911 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install uv
|
|
3
|
+
description: >-
|
|
4
|
+
Set up Python, then pip-install the uv version pinned in
|
|
5
|
+
.pre-commit-config.yaml. Needs the repo checked out first.
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
python-version:
|
|
9
|
+
description: the Python to set up and install uv into
|
|
10
|
+
required: true
|
|
11
|
+
|
|
12
|
+
outputs:
|
|
13
|
+
uv-version:
|
|
14
|
+
description: the uv version that was installed
|
|
15
|
+
value: ${{ steps.read.outputs.uv-version }}
|
|
16
|
+
|
|
17
|
+
runs:
|
|
18
|
+
using: composite
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ inputs.python-version }}
|
|
23
|
+
|
|
24
|
+
# Single source of truth for the uv version: the `rev` of the
|
|
25
|
+
# astral-sh/uv-pre-commit repo in .pre-commit-config.yaml, which
|
|
26
|
+
# `pre-commit autoupdate` already keeps current. Read with awk rather than
|
|
27
|
+
# a yaml parser so it needs nothing installed first. `rev` is SHA-pinned
|
|
28
|
+
# with the version in a trailing `# X.Y.Z` comment, so the version comes
|
|
29
|
+
# from that comment; fall back to the bare `rev` value for a tag-pinned rev
|
|
30
|
+
# with no comment.
|
|
31
|
+
- name: read uv version from .pre-commit-config.yaml
|
|
32
|
+
id: read
|
|
33
|
+
shell: bash
|
|
34
|
+
run: |
|
|
35
|
+
UV_VERSION="$(awk '
|
|
36
|
+
/^[[:space:]]*-[[:space:]]*repo:.*astral-sh\/uv-pre-commit[[:space:]]*$/ { found = 1; next }
|
|
37
|
+
found && $1 == "rev:" {
|
|
38
|
+
if (match($0, /#[ \t]*(frozen:[ \t]*)?[vV]?[0-9]+(\.[0-9]+)*/)) {
|
|
39
|
+
ver = substr($0, RSTART, RLENGTH)
|
|
40
|
+
sub(/^#[ \t]*(frozen:[ \t]*)?[vV]?/, "", ver)
|
|
41
|
+
} else {
|
|
42
|
+
ver = $2
|
|
43
|
+
gsub(/["'\'']/, "", ver)
|
|
44
|
+
sub(/^[vV]/, "", ver)
|
|
45
|
+
}
|
|
46
|
+
print ver
|
|
47
|
+
exit
|
|
48
|
+
}
|
|
49
|
+
' .pre-commit-config.yaml)"
|
|
50
|
+
if [ -z "$UV_VERSION" ]; then
|
|
51
|
+
echo "::error::could not read the astral-sh/uv-pre-commit version from .pre-commit-config.yaml"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
echo "uv-version=$UV_VERSION" >> "$GITHUB_OUTPUT"
|
|
55
|
+
echo "uv version: $UV_VERSION"
|
|
56
|
+
|
|
57
|
+
- name: install uv
|
|
58
|
+
shell: bash
|
|
59
|
+
env:
|
|
60
|
+
UV_VERSION: ${{ steps.read.outputs.uv-version }}
|
|
61
|
+
run: python -m pip install "uv==${UV_VERSION}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Keeps the SHA-pinned actions in .github/workflows/ current; Dependabot
|
|
2
|
+
# updates the trailing `# vX.Y.Z` comment along with the SHA.
|
|
3
|
+
#
|
|
4
|
+
# The 7-day cooldown is the point, not a courtesy: a release published minutes
|
|
5
|
+
# ago is the likeliest to be a compromised one.
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "github-actions"
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
12
|
+
cooldown:
|
|
13
|
+
default-days: 7
|
|
14
|
+
open-pull-requests-limit: 5
|
|
15
|
+
labels:
|
|
16
|
+
- "dependencies"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pre-commit
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
# so .github/workflows/tag_and_release.yaml can gate a release on this exact
|
|
11
|
+
# job rather than polling for other checks to go green
|
|
12
|
+
workflow_call:
|
|
13
|
+
inputs:
|
|
14
|
+
all-files:
|
|
15
|
+
description: run every hook on the whole tree instead of the pushed diff
|
|
16
|
+
type: boolean
|
|
17
|
+
default: false
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
concurrency:
|
|
23
|
+
# github.workflow keeps a workflow_call from tag_and_release.yaml out of the
|
|
24
|
+
# group a push to main starts for this workflow on its own
|
|
25
|
+
group: pre-commit-${{ github.workflow }}-${{ github.ref }}
|
|
26
|
+
cancel-in-progress: true
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
pre-commit:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
# no-commit-to-branch guards a developer's local `git commit`; it has
|
|
32
|
+
# nothing to check on a runner, and on a push to main it would fail the
|
|
33
|
+
# run of every merged PR. This repo doesn't use that hook today; the SKIP
|
|
34
|
+
# keeps CI working if it's added. zizmor is skipped here and runs in its
|
|
35
|
+
# own step at the end, the only one that gets GH_TOKEN. SKIP is read by
|
|
36
|
+
# pre-commit itself.
|
|
37
|
+
env:
|
|
38
|
+
SKIP: no-commit-to-branch,zizmor
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
41
|
+
with:
|
|
42
|
+
# required to grab the history of the PR for pre-commit
|
|
43
|
+
fetch-depth: 0
|
|
44
|
+
persist-credentials: false
|
|
45
|
+
|
|
46
|
+
# setup-python, then the uv version pinned in .pre-commit-config.yaml.
|
|
47
|
+
# `./` rather than zizmor's preferred `$/`, which actionlint v1.7.12
|
|
48
|
+
# rejects (rhysd/actionlint#711, #732), as in the analyzer.
|
|
49
|
+
# The local pyright and pytest hooks are `language: system` and run
|
|
50
|
+
# `uv run`, so uv has to be on PATH or both fail.
|
|
51
|
+
- uses: ./.github/actions/install-uv # zizmor: ignore[self-repository]
|
|
52
|
+
with:
|
|
53
|
+
python-version: "3.12"
|
|
54
|
+
|
|
55
|
+
# A PR or a push: the hooks on the changed files, including the
|
|
56
|
+
# manual-stage ones (pytest and the workflow linters).
|
|
57
|
+
# In a called workflow github.event_name is the caller's event, so a
|
|
58
|
+
# release started by a push would land here too; the all-files input is
|
|
59
|
+
# what sends it to the next step instead.
|
|
60
|
+
- name: pre-commit on the diff
|
|
61
|
+
if: >-
|
|
62
|
+
!inputs.all-files
|
|
63
|
+
&& (github.event_name == 'pull_request' || github.event_name == 'push')
|
|
64
|
+
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
|
65
|
+
with:
|
|
66
|
+
extra_args: >-
|
|
67
|
+
--color=always
|
|
68
|
+
--from-ref ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
69
|
+
--to-ref ${{ github.event.pull_request.head.sha || github.sha }}
|
|
70
|
+
--hook-stage manual
|
|
71
|
+
|
|
72
|
+
# manual dispatch, and tag_and_release.yaml: every hook against the full
|
|
73
|
+
# tree, not just a diff range
|
|
74
|
+
- name: pre-commit on all files
|
|
75
|
+
if: inputs.all-files || github.event_name == 'workflow_dispatch'
|
|
76
|
+
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
|
77
|
+
with:
|
|
78
|
+
extra_args: --color=always --all-files --hook-stage manual
|
|
79
|
+
|
|
80
|
+
# The two steps above cover pull_request, push and workflow_dispatch, and
|
|
81
|
+
# a workflow_call with all-files. Any other caller event would run no
|
|
82
|
+
# hooks at all, and pre-commit (installed by pre-commit/action) wouldn't
|
|
83
|
+
# be on PATH for the steps below; fail here and say why instead.
|
|
84
|
+
- name: no pre-commit run for this event
|
|
85
|
+
if: >-
|
|
86
|
+
!inputs.all-files
|
|
87
|
+
&& !contains(fromJSON('["pull_request", "push", "workflow_dispatch"]'), github.event_name)
|
|
88
|
+
env:
|
|
89
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
90
|
+
run: |
|
|
91
|
+
echo "::error::pre-commit.yaml has no diff range for a '${EVENT_NAME}' event; call it with all-files: true"
|
|
92
|
+
exit 1
|
|
93
|
+
|
|
94
|
+
# zizmor on its own, so GH_TOKEN reaches only it and not every hook
|
|
95
|
+
# (pytest and pyright run PR code). The token puts zizmor in online mode
|
|
96
|
+
# (offline is the default without one), so it can fetch the referenced
|
|
97
|
+
# actions for the audits that need them. The runner's GITHUB_TOKEN is
|
|
98
|
+
# enough for a public repo and is read-only per `permissions:` above.
|
|
99
|
+
# The workflow files are few, so this checks all of them every time.
|
|
100
|
+
- name: zizmor (online)
|
|
101
|
+
env:
|
|
102
|
+
SKIP: no-commit-to-branch
|
|
103
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
104
|
+
run: pre-commit run zizmor --color=always --all-files --hook-stage manual
|
|
105
|
+
|
|
106
|
+
# pre-commit/action restores the previous cache on a config change and
|
|
107
|
+
# saves it back under the new key, so every hook rev bump would leave its
|
|
108
|
+
# old environment in the cache forever. gc deletes the environments no
|
|
109
|
+
# live config references. Last on purpose: the cache is saved in the
|
|
110
|
+
# action's post step, after this.
|
|
111
|
+
- name: prune unused hook environments before the cache is saved
|
|
112
|
+
run: pre-commit gc
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Tags, releases and publishes to PyPI on a version bump. Nothing here bumps
|
|
3
|
+
# the version: change `[project] version` in pyproject.toml in a PR, merge it
|
|
4
|
+
# to main, and this workflow does the rest. A cut-down version of
|
|
5
|
+
# bigfix-relevance-analyzer's, which also builds a playground and a VS Code
|
|
6
|
+
# extension; here the wheel and sdist are the only assets.
|
|
7
|
+
name: Tag and Release
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches:
|
|
12
|
+
- main
|
|
13
|
+
paths:
|
|
14
|
+
- "pyproject.toml"
|
|
15
|
+
- "uv.lock"
|
|
16
|
+
- "src/**"
|
|
17
|
+
- ".github/workflows/tag_and_release.yaml"
|
|
18
|
+
- ".github/workflows/test.yaml"
|
|
19
|
+
- ".github/workflows/pre-commit.yaml"
|
|
20
|
+
- ".github/actions/**"
|
|
21
|
+
# for the first release, for re-running one that failed after the version
|
|
22
|
+
# was merged, and, with PYPI_PUBLISH unset, for testing everything up to
|
|
23
|
+
# the release job; the tag check below still makes it a no-op if the tag
|
|
24
|
+
# exists
|
|
25
|
+
workflow_dispatch:
|
|
26
|
+
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
|
|
30
|
+
concurrency:
|
|
31
|
+
# One release at a time, repo-wide. Releases only happen from main (see the
|
|
32
|
+
# `version` job), so keying on github.ref would only add a way to overlap.
|
|
33
|
+
# Serializing also keeps the tag check sound: a draft has no tag until it's
|
|
34
|
+
# published, so a second run that started mid-release would see no tag and
|
|
35
|
+
# try to release the same version again.
|
|
36
|
+
# Deliberately no cancel-in-progress: never kill a release mid-flight.
|
|
37
|
+
# GitHub keeps at most ONE pending run per group: a newer queued run
|
|
38
|
+
# replaces an older queued one (never the running one). The newer run
|
|
39
|
+
# releases whatever version its own commit has, so an intermediate bump
|
|
40
|
+
# whose run got replaced is skipped, not released late.
|
|
41
|
+
group: release
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
# Decides whether there is anything to do, so the jobs below can be skipped
|
|
45
|
+
# outright. A push that touches src/** without bumping the version gets a
|
|
46
|
+
# green, near-instant run and no release.
|
|
47
|
+
version:
|
|
48
|
+
name: Check for an untagged version
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
# Every later job needs this one, so skipping it skips the whole release.
|
|
51
|
+
# fork guard: a fork that pushes to its own main should not try to release.
|
|
52
|
+
# main guard: `push` is already limited to main, but workflow_dispatch can
|
|
53
|
+
# be started from any branch, and would release that branch's version.
|
|
54
|
+
# PYPI_PUBLISH gates only the `release` job (see there), so this job and
|
|
55
|
+
# the test and pre-commit gates run either way.
|
|
56
|
+
if: >-
|
|
57
|
+
github.repository == 'jgstew/bigfix-relevance-debugger'
|
|
58
|
+
&& github.ref == 'refs/heads/main'
|
|
59
|
+
outputs:
|
|
60
|
+
version: ${{ steps.getversion.outputs.version }}
|
|
61
|
+
prerelease: ${{ steps.getversion.outputs.prerelease }}
|
|
62
|
+
release_needed: ${{ steps.tagged.outputs.release_needed }}
|
|
63
|
+
steps:
|
|
64
|
+
# shallow: this job only reads pyproject.toml; the tag check asks the
|
|
65
|
+
# remote instead of needing every tag locally
|
|
66
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
67
|
+
with:
|
|
68
|
+
persist-credentials: false
|
|
69
|
+
|
|
70
|
+
# `[project] version` in pyproject.toml is the single source of truth:
|
|
71
|
+
# __version__ reads the installed package metadata, so there is no
|
|
72
|
+
# second place to bump. tomllib is stdlib on 3.11+, and the runner's
|
|
73
|
+
# preinstalled python is new enough, so no setup-python step is needed.
|
|
74
|
+
#
|
|
75
|
+
# Only the normalized PEP 440 spelling is accepted (X.Y.Z, then
|
|
76
|
+
# optionally aN/bN/rcN, .postN, .devN), so the tag v<version> matches
|
|
77
|
+
# the version PyPI and the wheel filename use: `1.0.0-beta` would be
|
|
78
|
+
# tagged as written but published as 1.0.0b0. A pre-release (a, b, rc or
|
|
79
|
+
# dev) becomes a GitHub pre-release, so it is never marked Latest.
|
|
80
|
+
- name: Read version from pyproject.toml
|
|
81
|
+
id: getversion
|
|
82
|
+
run: |
|
|
83
|
+
version="$(python3 -c 'import pathlib, tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
84
|
+
if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$'; then
|
|
85
|
+
echo "::error::'$version' in pyproject.toml isn't a normalized X.Y.Z[aN|bN|rcN][.postN][.devN] version"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
if printf '%s' "$version" | grep -Eq '(a|b|rc|\.dev)[0-9]+'; then
|
|
89
|
+
prerelease=true
|
|
90
|
+
else
|
|
91
|
+
prerelease=false
|
|
92
|
+
fi
|
|
93
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
94
|
+
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
|
|
95
|
+
echo "version: $version (prerelease: $prerelease)"
|
|
96
|
+
|
|
97
|
+
# Release only if this version has no tag yet. This is what makes the
|
|
98
|
+
# workflow idempotent and safe to re-run. `ls-remote --exit-code` exits
|
|
99
|
+
# 2 for "no such ref"; anything else non-zero is a real failure and
|
|
100
|
+
# must not be read as "untagged".
|
|
101
|
+
- name: Check whether v<version> is already tagged
|
|
102
|
+
id: tagged
|
|
103
|
+
env:
|
|
104
|
+
VERSION: ${{ steps.getversion.outputs.version }}
|
|
105
|
+
PYPI_PUBLISH: ${{ vars.PYPI_PUBLISH }}
|
|
106
|
+
run: |
|
|
107
|
+
rc=0
|
|
108
|
+
git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" > /dev/null || rc=$?
|
|
109
|
+
case "$rc" in
|
|
110
|
+
0)
|
|
111
|
+
echo "release_needed=false" >> "$GITHUB_OUTPUT"
|
|
112
|
+
echo "v${VERSION} is already tagged - nothing to release"
|
|
113
|
+
;;
|
|
114
|
+
2)
|
|
115
|
+
echo "release_needed=true" >> "$GITHUB_OUTPUT"
|
|
116
|
+
if [ "$PYPI_PUBLISH" = "true" ]; then
|
|
117
|
+
echo "v${VERSION} has no tag yet - will release"
|
|
118
|
+
else
|
|
119
|
+
msg="v${VERSION} has no tag yet, but PYPI_PUBLISH isn't \`true\`: the gates run, the release job is skipped"
|
|
120
|
+
echo "::notice::${msg}"
|
|
121
|
+
echo "$msg" >> "$GITHUB_STEP_SUMMARY"
|
|
122
|
+
fi
|
|
123
|
+
;;
|
|
124
|
+
*)
|
|
125
|
+
echo "::error::git ls-remote failed (exit $rc) checking for tag v${VERSION}"
|
|
126
|
+
exit 1
|
|
127
|
+
;;
|
|
128
|
+
esac
|
|
129
|
+
|
|
130
|
+
# The gates. Reusing the test and pre-commit workflows means the release is
|
|
131
|
+
# blocked by the exact same jobs that guard PRs (including the wheel smoke
|
|
132
|
+
# test on Linux, macOS and Windows), with no polling. Both run in parallel
|
|
133
|
+
# off `version`.
|
|
134
|
+
#
|
|
135
|
+
# The calls keep `./` and silence zizmor's self-repository audit, which
|
|
136
|
+
# wants GitHub's newer `$/` syntax that actionlint v1.7.12 rejects
|
|
137
|
+
# (rhysd/actionlint#711, #732), as in the analyzer.
|
|
138
|
+
test:
|
|
139
|
+
name: Test
|
|
140
|
+
needs: version
|
|
141
|
+
if: needs.version.outputs.release_needed == 'true'
|
|
142
|
+
uses: ./.github/workflows/test.yaml # zizmor: ignore[self-repository]
|
|
143
|
+
|
|
144
|
+
pre-commit:
|
|
145
|
+
name: pre-commit
|
|
146
|
+
needs: version
|
|
147
|
+
if: needs.version.outputs.release_needed == 'true'
|
|
148
|
+
uses: ./.github/workflows/pre-commit.yaml # zizmor: ignore[self-repository]
|
|
149
|
+
with:
|
|
150
|
+
# a push only carries its own diff; a release checks the whole tree
|
|
151
|
+
all-files: true
|
|
152
|
+
|
|
153
|
+
release:
|
|
154
|
+
name: Tag, release and publish
|
|
155
|
+
needs: [version, test, pre-commit]
|
|
156
|
+
# The only job PYPI_PUBLISH gates. It does all the outward-facing work
|
|
157
|
+
# (tag, GitHub release, PyPI upload), so with the repository variable
|
|
158
|
+
# unset (Settings > Secrets and variables > Actions > Variables) a run
|
|
159
|
+
# still exercises the version check and both gates on the real trigger,
|
|
160
|
+
# and creates nothing. This gates the whole job, not just the PyPI step
|
|
161
|
+
# as the analyzer does: for this repo PyPI is the point of a release, and
|
|
162
|
+
# a GitHub-only release would tag the version, so the tag check would
|
|
163
|
+
# never publish it to PyPI later. (`needs` still requires the gates to
|
|
164
|
+
# have succeeded; an `if:` without a status function implies success().)
|
|
165
|
+
if: vars.PYPI_PUBLISH == 'true'
|
|
166
|
+
runs-on: ubuntu-latest
|
|
167
|
+
# Gates the whole job - tag, GitHub release and PyPI upload - so an
|
|
168
|
+
# environment protection rule (required reviewer, branch restriction)
|
|
169
|
+
# stops a release before anything is created. The name must match the
|
|
170
|
+
# `Environment name` of the PyPI trusted publisher exactly: PyPI checks the
|
|
171
|
+
# `environment` claim in the OIDC token.
|
|
172
|
+
environment: pypi
|
|
173
|
+
permissions:
|
|
174
|
+
# contents: write - create the release (and so the tag) and upload assets
|
|
175
|
+
# id-token: write - OIDC for PyPI trusted publishing
|
|
176
|
+
contents: write
|
|
177
|
+
id-token: write
|
|
178
|
+
steps:
|
|
179
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
180
|
+
with:
|
|
181
|
+
persist-credentials: false
|
|
182
|
+
|
|
183
|
+
# setup-python, then the uv version pinned in .pre-commit-config.yaml;
|
|
184
|
+
# the same action test.yaml and pre-commit.yaml use
|
|
185
|
+
- uses: ./.github/actions/install-uv # zizmor: ignore[self-repository]
|
|
186
|
+
with:
|
|
187
|
+
python-version: "3.13"
|
|
188
|
+
|
|
189
|
+
# The exact bytes the gates tested: test.yaml's `build` job uploaded
|
|
190
|
+
# them and its smoke matrix ran them on Linux, macOS and Windows. No
|
|
191
|
+
# second `uv build` here: the wheel records the hatchling that built it
|
|
192
|
+
# (WHEEL's Generator), so a rebuild after a hatchling release would ship
|
|
193
|
+
# untested bytes, and a re-run's rebuild would no longer match what an
|
|
194
|
+
# earlier attempt already put on PyPI. Artifacts belong to the run, so
|
|
195
|
+
# "Re-run failed jobs" downloads the first attempt's.
|
|
196
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
197
|
+
with:
|
|
198
|
+
name: test-dist
|
|
199
|
+
path: dist
|
|
200
|
+
|
|
201
|
+
# `sha256sum -c`-verifiable, with no directory prefix, so it checks out
|
|
202
|
+
# wherever the assets are downloaded to
|
|
203
|
+
- name: Compute SHA-256 checksums
|
|
204
|
+
working-directory: dist
|
|
205
|
+
run: |
|
|
206
|
+
sha256sum ./*.whl ./*.tar.gz > SHA256SUMS.txt
|
|
207
|
+
cat SHA256SUMS.txt
|
|
208
|
+
|
|
209
|
+
# PyPI never replaces a file. If an earlier attempt already uploaded this
|
|
210
|
+
# version from other bytes (a newer commit on main, or a rebuild), stop
|
|
211
|
+
# here, before the draft's assets are touched; `uv publish --check-url`
|
|
212
|
+
# would only catch it after they had been replaced. Files PyPI doesn't
|
|
213
|
+
# have yet, or has with the same hash, are fine: that's a re-run
|
|
214
|
+
# finishing a partial release. Also checks dist/ is for this version.
|
|
215
|
+
- name: Check PyPI doesn't already have different bytes for this version
|
|
216
|
+
env:
|
|
217
|
+
PACKAGE: bigfix-relevance-debugger
|
|
218
|
+
VERSION: ${{ needs.version.outputs.version }}
|
|
219
|
+
run: |
|
|
220
|
+
python - <<'EOF'
|
|
221
|
+
import hashlib
|
|
222
|
+
import json
|
|
223
|
+
import os
|
|
224
|
+
import pathlib
|
|
225
|
+
import sys
|
|
226
|
+
import urllib.error
|
|
227
|
+
import urllib.request
|
|
228
|
+
|
|
229
|
+
package, version = os.environ["PACKAGE"], os.environ["VERSION"]
|
|
230
|
+
stem = package.replace("-", "_") + "-" + version
|
|
231
|
+
local = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in pathlib.Path("dist").glob(f"{stem}[-.]*")}
|
|
232
|
+
if not any(n.endswith(".whl") for n in local) or not any(n.endswith(".tar.gz") for n in local):
|
|
233
|
+
sys.exit(f"::error::dist/ has no wheel and sdist for {version}: {sorted(local)}")
|
|
234
|
+
try:
|
|
235
|
+
with urllib.request.urlopen(f"https://pypi.org/pypi/{package}/{version}/json", timeout=30) as r:
|
|
236
|
+
released = {f["filename"]: f["digests"]["sha256"] for f in json.load(r)["urls"]}
|
|
237
|
+
except urllib.error.HTTPError as e:
|
|
238
|
+
if e.code != 404:
|
|
239
|
+
raise
|
|
240
|
+
released = {}
|
|
241
|
+
differ = sorted(n for n, h in local.items() if n in released and released[n] != h)
|
|
242
|
+
if differ:
|
|
243
|
+
sys.exit(
|
|
244
|
+
f"::error::PyPI already has {', '.join(differ)} for {version} with different bytes. "
|
|
245
|
+
"PyPI never replaces a file: bump the version, or re-run the original run's failed jobs."
|
|
246
|
+
)
|
|
247
|
+
for name in sorted(local):
|
|
248
|
+
print(f"{name}: {'already on PyPI (identical)' if name in released else 'not on PyPI yet'}")
|
|
249
|
+
EOF
|
|
250
|
+
|
|
251
|
+
# Created as a DRAFT and published only at the end, once PyPI has the
|
|
252
|
+
# package. That keeps this compatible with GitHub's immutable releases,
|
|
253
|
+
# which lock a release's assets at publication. A draft does not create
|
|
254
|
+
# its tag either: the target is recorded now, the tag appears on publish.
|
|
255
|
+
#
|
|
256
|
+
# A re-run that finds the draft from a failed attempt reuses it: it
|
|
257
|
+
# moves the target to this run's commit (a run from a newer main would
|
|
258
|
+
# otherwise tag the old commit next to the new commit's assets) and
|
|
259
|
+
# replaces the assets (--clobber). The lookup lists releases with
|
|
260
|
+
# `gh api`, so a failed request fails the step rather than reading as
|
|
261
|
+
# "no draft" and creating a second one.
|
|
262
|
+
- name: Create or reuse the draft GitHub release
|
|
263
|
+
env:
|
|
264
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
265
|
+
VERSION: ${{ needs.version.outputs.version }}
|
|
266
|
+
PRERELEASE: ${{ needs.version.outputs.prerelease }}
|
|
267
|
+
run: |
|
|
268
|
+
drafts="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" \
|
|
269
|
+
--jq ".[] | select(.draft and .tag_name == \"v${VERSION}\") | .id")"
|
|
270
|
+
count="$(printf '%s' "$drafts" | grep -c . || true)"
|
|
271
|
+
case "$count" in
|
|
272
|
+
0)
|
|
273
|
+
gh release create "v${VERSION}" --repo "$GITHUB_REPOSITORY" \
|
|
274
|
+
--draft \
|
|
275
|
+
--prerelease="${PRERELEASE}" \
|
|
276
|
+
--target "${GITHUB_SHA}" \
|
|
277
|
+
--title "v${VERSION}" \
|
|
278
|
+
--notes "Draft: publishing to PyPI." \
|
|
279
|
+
dist/*.whl dist/*.tar.gz dist/SHA256SUMS.txt
|
|
280
|
+
;;
|
|
281
|
+
1)
|
|
282
|
+
echo "draft v${VERSION} (id ${drafts}) exists from an earlier attempt; retargeting it to ${GITHUB_SHA} and replacing its assets"
|
|
283
|
+
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${drafts}" \
|
|
284
|
+
-f "target_commitish=${GITHUB_SHA}" \
|
|
285
|
+
-F "prerelease=${PRERELEASE}" > /dev/null
|
|
286
|
+
gh release upload "v${VERSION}" --repo "$GITHUB_REPOSITORY" --clobber \
|
|
287
|
+
dist/*.whl dist/*.tar.gz dist/SHA256SUMS.txt
|
|
288
|
+
;;
|
|
289
|
+
*)
|
|
290
|
+
echo "::error::${count} draft releases are tagged v${VERSION} (ids: ${drafts//$'\n'/ }); delete the extras and re-run"
|
|
291
|
+
exit 1
|
|
292
|
+
;;
|
|
293
|
+
esac
|
|
294
|
+
|
|
295
|
+
# Trusted publishing: `uv publish` speaks OIDC natively given id-token:
|
|
296
|
+
# write, so there is no long-lived API token to store and no
|
|
297
|
+
# pypa/gh-action-pypi-publish pin to maintain. Needs a PyPI trusted
|
|
298
|
+
# publisher (or pending publisher) for this repo with workflow
|
|
299
|
+
# `tag_and_release.yaml` and environment `pypi`.
|
|
300
|
+
# The explicit globs keep SHA256SUMS.txt out of the upload. --check-url
|
|
301
|
+
# skips files PyPI already has, so a re-run after a partial upload
|
|
302
|
+
# finishes the job instead of failing on "File already exists".
|
|
303
|
+
- name: Publish to PyPI (trusted publishing)
|
|
304
|
+
run: >-
|
|
305
|
+
uv publish --trusted-publishing always
|
|
306
|
+
--check-url https://pypi.org/simple/
|
|
307
|
+
dist/*.whl dist/*.tar.gz
|
|
308
|
+
|
|
309
|
+
# GitHub's own generated notes with the checksums appended, so the
|
|
310
|
+
# hashes are readable in the release body as well as downloadable.
|
|
311
|
+
- name: Build release notes
|
|
312
|
+
env:
|
|
313
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
314
|
+
VERSION: ${{ needs.version.outputs.version }}
|
|
315
|
+
run: |
|
|
316
|
+
notes_file="${RUNNER_TEMP}/release-notes.md"
|
|
317
|
+
if ! gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
|
|
318
|
+
--method POST \
|
|
319
|
+
-f "tag_name=v${VERSION}" \
|
|
320
|
+
-f "target_commitish=${GITHUB_SHA}" \
|
|
321
|
+
--jq '.body' > "$notes_file"; then
|
|
322
|
+
echo "::warning::could not generate release notes; falling back to a plain body"
|
|
323
|
+
printf 'Release v%s\n' "${VERSION}" > "$notes_file"
|
|
324
|
+
fi
|
|
325
|
+
# The backticks below are Markdown in release-note text, not command
|
|
326
|
+
# substitutions; single quotes keep them literal.
|
|
327
|
+
# shellcheck disable=SC2016
|
|
328
|
+
{
|
|
329
|
+
printf '\n## Install\n\n'
|
|
330
|
+
printf '```bash\nuvx bigfix-relevance-debugger@%s\n```\n' "${VERSION}"
|
|
331
|
+
printf '\n## SHA-256\n\n'
|
|
332
|
+
printf 'Verify with `sha256sum -c SHA256SUMS.txt`:\n\n'
|
|
333
|
+
printf '```\n'
|
|
334
|
+
cat dist/SHA256SUMS.txt
|
|
335
|
+
printf '```\n'
|
|
336
|
+
} >> "$notes_file"
|
|
337
|
+
echo "NOTES_FILE=$notes_file" >> "$GITHUB_ENV"
|
|
338
|
+
cat "$notes_file"
|
|
339
|
+
|
|
340
|
+
# Publishing is the last step and the point where the tag is created
|
|
341
|
+
# and, with immutable releases on, everything is locked.
|
|
342
|
+
- name: Publish the GitHub release
|
|
343
|
+
env:
|
|
344
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
345
|
+
VERSION: ${{ needs.version.outputs.version }}
|
|
346
|
+
PRERELEASE: ${{ needs.version.outputs.prerelease }}
|
|
347
|
+
run: |
|
|
348
|
+
gh release edit "v${VERSION}" --repo "$GITHUB_REPOSITORY" \
|
|
349
|
+
--notes-file "${NOTES_FILE}" \
|
|
350
|
+
--prerelease="${PRERELEASE}" \
|
|
351
|
+
--draft=false
|