releaseradar 2026.1__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.
Files changed (63) hide show
  1. releaseradar-2026.1/.gitattributes +3 -0
  2. releaseradar-2026.1/.github/scripts/wait_for_pypi.sh +36 -0
  3. releaseradar-2026.1/.github/workflows/bandit.yml +36 -0
  4. releaseradar-2026.1/.github/workflows/codeql.yml +35 -0
  5. releaseradar-2026.1/.github/workflows/docker-build-check.yml +83 -0
  6. releaseradar-2026.1/.github/workflows/docker-scout.yml +44 -0
  7. releaseradar-2026.1/.github/workflows/docker.yml +81 -0
  8. releaseradar-2026.1/.github/workflows/licenses.yml +33 -0
  9. releaseradar-2026.1/.github/workflows/mutation.yml +42 -0
  10. releaseradar-2026.1/.github/workflows/publish.yml +65 -0
  11. releaseradar-2026.1/.github/workflows/quality.yml +33 -0
  12. releaseradar-2026.1/.github/workflows/release.yml +47 -0
  13. releaseradar-2026.1/.github/workflows/security-posture.yml +120 -0
  14. releaseradar-2026.1/.github/workflows/snyk.yml +205 -0
  15. releaseradar-2026.1/.github/workflows/sonarcloud.yml +50 -0
  16. releaseradar-2026.1/.github/workflows/test.yml +77 -0
  17. releaseradar-2026.1/.github/workflows/trivy.yml +30 -0
  18. releaseradar-2026.1/.gitignore +109 -0
  19. releaseradar-2026.1/CHANGELOG.md +74 -0
  20. releaseradar-2026.1/Dockerfile +69 -0
  21. releaseradar-2026.1/LICENSE +21 -0
  22. releaseradar-2026.1/PKG-INFO +219 -0
  23. releaseradar-2026.1/README.md +157 -0
  24. releaseradar-2026.1/SECURITY-EXCEPTIONS.toml +80 -0
  25. releaseradar-2026.1/SECURITY.md +22 -0
  26. releaseradar-2026.1/pyproject.toml +110 -0
  27. releaseradar-2026.1/release.sh +108 -0
  28. releaseradar-2026.1/releaseradar/__init__.py +3 -0
  29. releaseradar-2026.1/releaseradar/cli.py +303 -0
  30. releaseradar-2026.1/releaseradar/db.py +311 -0
  31. releaseradar-2026.1/releaseradar/github.py +223 -0
  32. releaseradar-2026.1/releaseradar/mcp_server.py +174 -0
  33. releaseradar-2026.1/releaseradar/sync.py +129 -0
  34. releaseradar-2026.1/sonar-project.properties +20 -0
  35. releaseradar-2026.1/tests/__init__.py +0 -0
  36. releaseradar-2026.1/tests/conftest.py +68 -0
  37. releaseradar-2026.1/tests/docker/inspect.sh +87 -0
  38. releaseradar-2026.1/tests/docker/smoke.py +20 -0
  39. releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_issues_all.json +18 -0
  40. releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_pulls_all.json +18 -0
  41. releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_releases.json +18 -0
  42. releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_repo.json +158 -0
  43. releaseradar-2026.1/tests/fixtures/gh_cookieradar_releases.json +738 -0
  44. releaseradar-2026.1/tests/fixtures/gh_missing_repo_404.json +22 -0
  45. releaseradar-2026.1/tests/fixtures/gh_missing_repo_issues_404.json +22 -0
  46. releaseradar-2026.1/tests/fixtures/gh_requests_issues_open_5.json +416 -0
  47. releaseradar-2026.1/tests/fixtures/gh_requests_pulls_open_5.json +1910 -0
  48. releaseradar-2026.1/tests/test_ci_contract.py +176 -0
  49. releaseradar-2026.1/tests/test_cli.py +220 -0
  50. releaseradar-2026.1/tests/test_db.py +264 -0
  51. releaseradar-2026.1/tests/test_declared_imports.py +151 -0
  52. releaseradar-2026.1/tests/test_docker_contract.py +128 -0
  53. releaseradar-2026.1/tests/test_github.py +200 -0
  54. releaseradar-2026.1/tests/test_mcp_server.py +181 -0
  55. releaseradar-2026.1/tests/test_project_metadata.py +57 -0
  56. releaseradar-2026.1/tests/test_release_script.py +402 -0
  57. releaseradar-2026.1/tests/test_security_exceptions.py +379 -0
  58. releaseradar-2026.1/tests/test_sync.py +205 -0
  59. releaseradar-2026.1/tests/test_version_contract.py +149 -0
  60. releaseradar-2026.1/tests/test_workflow_set.py +103 -0
  61. releaseradar-2026.1/tools/security_exceptions.py +457 -0
  62. releaseradar-2026.1/tools/snyk_ids.py +65 -0
  63. releaseradar-2026.1/tools/snyk_requirements.py +117 -0
@@ -0,0 +1,3 @@
1
+ # Shell scripts run under bash, which reads a CR as part of the line: checked
2
+ # out with core.autocrlf=true, release.sh died on `set -o pipefail\r`.
3
+ *.sh text eol=lf
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Wait until <package>==<version> can be downloaded from PyPI.
3
+ # Uses pip itself, so it checks exactly what `pip install` in the Dockerfile needs.
4
+ # Usage: wait_for_pypi.sh <package> <version> [attempts=30] [interval_seconds=20] [grace_seconds=45]
5
+ set -uo pipefail
6
+
7
+ if [ $# -lt 2 ]; then
8
+ echo "Usage: $0 <package> <version> [attempts] [interval_seconds]" >&2
9
+ exit 2
10
+ fi
11
+
12
+ PACKAGE="$1"
13
+ VERSION="$2"
14
+ ATTEMPTS="${3:-30}"
15
+ INTERVAL="${4:-20}"
16
+ GRACE="${5:-45}"
17
+ DEST="$(mktemp -d)"
18
+ trap 'rm -rf "$DEST"' EXIT
19
+
20
+ for attempt in $(seq 1 "$ATTEMPTS"); do
21
+ if pip download --no-deps --no-cache-dir --disable-pip-version-check --quiet --dest "$DEST" "${PACKAGE}==${VERSION}"; then
22
+ echo "${PACKAGE}==${VERSION} available on PyPI (attempt ${attempt}/${ATTEMPTS})"
23
+ if [ "$GRACE" -gt 0 ]; then
24
+ echo "waiting ${GRACE}s for the index to agree with itself"
25
+ sleep "$GRACE"
26
+ fi
27
+ exit 0
28
+ fi
29
+ echo "${PACKAGE}==${VERSION} not yet available (attempt ${attempt}/${ATTEMPTS})"
30
+ if [ "$attempt" -lt "$ATTEMPTS" ]; then
31
+ sleep "$INTERVAL"
32
+ fi
33
+ done
34
+
35
+ echo "${PACKAGE}==${VERSION} still not available on PyPI after ${ATTEMPTS} attempts" >&2
36
+ exit 1
@@ -0,0 +1,36 @@
1
+ name: Bandit Security Scan
2
+ on:
3
+ push:
4
+ branches: [ main ]
5
+ pull_request:
6
+ branches: [ main ]
7
+ schedule:
8
+ - cron: '30 3 * * 1'
9
+ permissions:
10
+ contents: read
11
+ security-events: write
12
+ jobs:
13
+ bandit:
14
+ name: Bandit
15
+ runs-on: ubuntu-26.04
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v7
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v7
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Install Bandit
24
+ run: pip install bandit[sarif]
25
+ - name: Run Bandit
26
+ run: |
27
+ bandit -r releaseradar/ \
28
+ -f sarif \
29
+ -o bandit-results.sarif \
30
+ --severity-level medium \
31
+ --exit-zero
32
+ - name: Upload Bandit results to GitHub Security tab
33
+ uses: github/codeql-action/upload-sarif@v4
34
+ if: always() && hashFiles('bandit-results.sarif') != ''
35
+ with:
36
+ sarif_file: bandit-results.sarif
@@ -0,0 +1,35 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: '0 6 * * 1'
10
+
11
+ jobs:
12
+ analyze:
13
+ name: Analyze
14
+ runs-on: ubuntu-26.04
15
+ permissions:
16
+ actions: read
17
+ contents: read
18
+ security-events: write
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
23
+
24
+ - name: Initialize CodeQL
25
+ uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
26
+ with:
27
+ languages: python
28
+
29
+ - name: Autobuild
30
+ uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
31
+
32
+ - name: Perform CodeQL Analysis
33
+ uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
34
+ with:
35
+ category: "/language:python"
@@ -0,0 +1,83 @@
1
+ name: Docker Build Check
2
+ # Builds the image and runs the smoke test without pushing anything. docker.yml
3
+ # cannot serve this purpose: it publishes to Docker Hub, including the :latest
4
+ # tag, so it is only safe to run for an already released version.
5
+ #
6
+ # The Dockerfile installs releaseradar from PyPI rather than from this checkout, so
7
+ # the build needs a published version. It is resolved here rather than taken
8
+ # from the Dockerfile's RELEASERADAR_VERSION default, which lags many releases behind.
9
+ # On every push and pull request, because the only other thing that builds this image
10
+ # is docker.yml, which publishes as it builds — so until this ran on its own, the first
11
+ # attempt at a build was the one that released. It could not run on its own before: the
12
+ # Dockerfile only knew how to install from the index, so it had to be handed a published
13
+ # version to build anything at all.
14
+ #
15
+ # Given a version it still builds that one from the index, which is how an older image
16
+ # is reproduced. Given nothing it builds this checkout.
17
+ on:
18
+ push:
19
+ branches: [ main ]
20
+ pull_request:
21
+ branches: [ main ]
22
+ workflow_dispatch:
23
+ inputs:
24
+ version:
25
+ description: 'Published version to reproduce (default: build this checkout)'
26
+ required: false
27
+ permissions:
28
+ contents: read
29
+ jobs:
30
+ build:
31
+ runs-on: ubuntu-26.04
32
+ steps:
33
+ - name: Checkout
34
+ uses: actions/checkout@v7
35
+
36
+ - name: Resolve the version to build
37
+ id: ver
38
+ env:
39
+ REQUESTED: ${{ inputs.version }}
40
+ run: |
41
+ if [ -n "$REQUESTED" ]; then
42
+ SOURCE=pypi
43
+ VERSION="${REQUESTED#v}"
44
+ else
45
+ SOURCE=local
46
+ VERSION=$(python3 -c "import releaseradar; print(releaseradar.__version__)")
47
+ fi
48
+ echo "source=$SOURCE" >> "$GITHUB_OUTPUT"
49
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
50
+ echo "Building releaseradar $VERSION"
51
+
52
+ - name: Set up Docker Buildx
53
+ uses: docker/setup-buildx-action@v4
54
+
55
+ - name: Build image
56
+ uses: docker/build-push-action@v7
57
+ with:
58
+ context: .
59
+ load: true
60
+ push: false
61
+ tags: releaseradar:build-check
62
+ build-args: |
63
+ RELEASERADAR_SOURCE=${{ steps.ver.outputs.source }}
64
+ RELEASERADAR_VERSION=${{ steps.ver.outputs.version }}
65
+
66
+ - name: Smoke test (the CLI runs as the container user)
67
+ env:
68
+ EXPECTED_VERSION: ${{ steps.ver.outputs.version }}
69
+ run: |
70
+ docker run --rm releaseradar:build-check --help > /dev/null
71
+ docker run --rm -i -e EXPECTED_VERSION --entrypoint python releaseradar:build-check - < tests/docker/smoke.py
72
+
73
+ # What the image contains, read from the image itself. This is the only place
74
+ # it can be read: Docker Desktop will not install on the machine this is
75
+ # developed on, where Windows 10 IoT Enterprise LTSC 2021 is build 19044 and
76
+ # Docker requires 19045 — a build that edition never receives. A Linux runner
77
+ # is the truer place for the question anyway.
78
+ #
79
+ # See tests/docker/inspect.sh for what it settles: SECURITY-EXCEPTIONS.toml
80
+ # answers two of its claims with evidence borrowed from patchradar's image
81
+ # rather than measured in this one.
82
+ - name: What the image contains
83
+ run: docker run --rm -i --entrypoint sh releaseradar:build-check - < tests/docker/inspect.sh
@@ -0,0 +1,44 @@
1
+ name: Docker Scout CVE Scan
2
+ on:
3
+ # The scan reads maksimtech/releaseradar:latest from Docker Hub, so it is only
4
+ # meaningful once a new image is published — which happens on a tag, not on a
5
+ # push to main. Without this, re-scanning after a release meant re-running an
6
+ # earlier run by hand.
7
+ workflow_dispatch:
8
+ push:
9
+ branches: [ main ]
10
+ schedule:
11
+ - cron: '30 4 * * 1'
12
+ permissions:
13
+ contents: read
14
+ security-events: write
15
+ jobs:
16
+ scout:
17
+ name: Docker Scout
18
+ runs-on: ubuntu-26.04
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v7
22
+ - name: Login to Docker Hub
23
+ uses: docker/login-action@v4
24
+ with:
25
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
26
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
27
+ - name: Docker Scout CVE scan
28
+ uses: docker/scout-action@v1
29
+ with:
30
+ # cves says what is broken in the image; recommendations says
31
+ # whether a base image exists that fixes it. On 2026-09-26 the first
32
+ # scan of exeradar found five HIGH CVEs, all of them in
33
+ # python:3.14-slim-trixie and none in this project's code — which is
34
+ # a fact nobody can act on without the second command.
35
+ command: cves,recommendations
36
+ image: maksimtech/releaseradar:latest
37
+ only-severities: critical,high
38
+ exit-code: false
39
+ sarif-file: scout-results.sarif
40
+ - name: Upload Scout results to GitHub Security tab
41
+ uses: github/codeql-action/upload-sarif@v4
42
+ if: always() && hashFiles('scout-results.sarif') != ''
43
+ with:
44
+ sarif_file: scout-results.sarif
@@ -0,0 +1,81 @@
1
+ name: Build and Push Docker Image
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*'
6
+ schedule:
7
+ # Weekly rebuild of `latest` from the newest tag so the image picks up
8
+ # Debian's patches without waiting for a new release.
9
+ - cron: '0 5 * * 1'
10
+ workflow_dispatch:
11
+ inputs:
12
+ version:
13
+ description: 'Version tag (e.g. v2026.1)'
14
+ required: true
15
+ default: 'v2026.1'
16
+ permissions:
17
+ contents: read
18
+ security-events: write
19
+ jobs:
20
+ docker:
21
+ runs-on: ubuntu-26.04
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v7
25
+ with:
26
+ fetch-depth: 0
27
+ - name: Set up QEMU
28
+ uses: docker/setup-qemu-action@v4
29
+ - name: Set up Docker Buildx
30
+ uses: docker/setup-buildx-action@v4
31
+ - name: Login to Docker Hub
32
+ uses: docker/login-action@v4
33
+ with:
34
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
35
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
36
+ - name: Extract version
37
+ id: version
38
+ run: |
39
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
40
+ RAW="${{ github.event.inputs.version }}"
41
+ elif [ "${{ github.event_name }}" = "schedule" ]; then
42
+ RAW=$(git tag --list 'v*' --sort=-v:refname | head -n 1)
43
+ else
44
+ RAW="${GITHUB_REF#refs/tags/}"
45
+ fi
46
+ echo "VERSION=${RAW}" >> $GITHUB_OUTPUT
47
+ STRIPPED="${RAW#v}"
48
+ NORMALIZED=$(python3 -c "parts='${STRIPPED}'.split('.'); print('.'.join(str(int(p)) for p in parts))")
49
+ echo "PIP_VERSION=${NORMALIZED}" >> $GITHUB_OUTPUT
50
+ - name: Stand on the tag being built
51
+ env:
52
+ VERSION: ${{ steps.version.outputs.VERSION }}
53
+ run: git checkout --detach "refs/tags/${VERSION}"
54
+ - name: Build and push
55
+ uses: docker/build-push-action@v7
56
+ with:
57
+ context: .
58
+ platforms: linux/amd64,linux/arm64
59
+ push: true
60
+ tags: |
61
+ maksimtech/releaseradar:latest
62
+ ${{ github.event_name != 'schedule' && format('maksimtech/releaseradar:{0}', steps.version.outputs.VERSION) || '' }}
63
+ build-args: |
64
+ RELEASERADAR_SOURCE=local
65
+ pull: true
66
+ no-cache: ${{ github.event_name == 'schedule' }}
67
+ cache-from: type=gha
68
+ cache-to: type=gha,mode=max
69
+ - name: Docker Scout CVE scan
70
+ uses: docker/scout-action@v1
71
+ with:
72
+ command: cves
73
+ image: maksimtech/releaseradar:${{ github.event_name == 'schedule' && 'latest' || steps.version.outputs.VERSION }}
74
+ only-severities: critical,high
75
+ exit-code: false
76
+ sarif-file: scout-results.sarif
77
+ - name: Upload Scout SARIF
78
+ uses: github/codeql-action/upload-sarif@v4
79
+ if: always()
80
+ with:
81
+ sarif_file: scout-results.sarif
@@ -0,0 +1,33 @@
1
+ name: License Check
2
+ on:
3
+ push:
4
+ branches: [ main ]
5
+ pull_request:
6
+ branches: [ main ]
7
+ permissions:
8
+ contents: read
9
+ jobs:
10
+ licenses:
11
+ name: License Check
12
+ runs-on: ubuntu-26.04
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v7
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v7
18
+ with:
19
+ python-version: "3.12"
20
+ - name: Install dependencies
21
+ run: |
22
+ pip install -e . # NOSONAR
23
+ pip install pip-licenses
24
+ - name: Check licenses
25
+ run: |
26
+ pip-licenses \
27
+ --order=license \
28
+ --format=markdown \
29
+ --output-file=licenses.md
30
+ cat licenses.md
31
+ - name: Fail on forbidden licenses
32
+ run: |
33
+ pip-licenses --fail-on="GPL;AGPL;LGPL"
@@ -0,0 +1,42 @@
1
+ name: Mutation testing
2
+ on:
3
+ # Off the critical path on purpose: a mutation run is measured in hours, and
4
+ # its result is a reading on the test suite rather than a gate on a change.
5
+ schedule:
6
+ - cron: '0 4 * * 6'
7
+ workflow_dispatch:
8
+ permissions:
9
+ contents: read
10
+ jobs:
11
+ mutmut:
12
+ name: mutmut
13
+ runs-on: ubuntu-26.04
14
+ # mutmut refuses to run on Windows and points at WSL, so ubuntu is not a
15
+ # preference here — it is the only host that works.
16
+ timeout-minutes: 350
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v7
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Install dependencies
24
+ run: |
25
+ python -m pip install --upgrade pip
26
+ pip install -e ".[dev]" # NOSONAR - trusted internal project, no external setup scripts
27
+ - name: Run mutation testing
28
+ # A surviving mutant is a finding, not a failure: mutmut exits 0 and
29
+ # prints the score. The job still fails if mutmut itself crashes, which
30
+ # is the distinction "|| true" would have destroyed.
31
+ run: mutmut run --max-children 4
32
+ - name: Export the score
33
+ run: mutmut export-cicd-stats
34
+ - name: Show surviving mutants
35
+ run: mutmut results
36
+ - name: Upload the report
37
+ uses: actions/upload-artifact@v7
38
+ if: always()
39
+ with:
40
+ name: mutmut-stats
41
+ path: mutants/mutmut-cicd-stats.json
42
+ if-no-files-found: warn
@@ -0,0 +1,65 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*'
6
+ workflow_dispatch:
7
+ jobs:
8
+ build-and-publish:
9
+ name: Build and publish to PyPI
10
+ runs-on: ubuntu-26.04
11
+ environment:
12
+ name: pypi
13
+ url: https://pypi.org/p/releaseradar
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v7
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v7
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Install build dependencies
24
+ run: pip install --upgrade build hatchling # NOSONAR
25
+ - name: Build package
26
+ run: python -m build
27
+ - name: Import GPG key
28
+ run: |
29
+ echo "${{ secrets.GPG_PRIVATE_KEY }}" > /tmp/gpg_key.asc
30
+ gpg --batch --import /tmp/gpg_key.asc
31
+ rm /tmp/gpg_key.asc
32
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --passphrase-fd 0 --pinentry-mode loopback --list-secret-keys
33
+ - name: Sign packages
34
+ run: |
35
+ for f in dist/*; do
36
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --passphrase-fd 0 \
37
+ --pinentry-mode loopback \
38
+ --armor --detach-sign "$f"
39
+ done
40
+ - name: Publish to PyPI
41
+ uses: pypa/gh-action-pypi-publish@release/v1
42
+ with:
43
+ verify-metadata: false
44
+
45
+ - name: Extract version
46
+ id: version
47
+ env:
48
+ RAW: ${{ github.ref_name }}
49
+ run: |
50
+ STRIPPED="${RAW#v}"
51
+ NORMALIZED=$(python3 -c "parts='${STRIPPED}'.split('.'); print('.'.join(str(int(p)) for p in parts))")
52
+ echo "VERSION=${NORMALIZED}" >> "$GITHUB_OUTPUT"
53
+
54
+ - name: Wait for the index to catch up
55
+ env:
56
+ VERSION: ${{ steps.version.outputs.VERSION }}
57
+ run: bash .github/scripts/wait_for_pypi.sh releaseradar "$VERSION" 30 20 0
58
+
59
+ - name: Check the published wheel installs and runs
60
+ env:
61
+ VERSION: ${{ steps.version.outputs.VERSION }}
62
+ run: |
63
+ python -m venv "${RUNNER_TEMP}/verify"
64
+ "${RUNNER_TEMP}/verify/bin/pip" install --no-cache-dir "releaseradar==${VERSION}"
65
+ "${RUNNER_TEMP}/verify/bin/releaseradar" --version
@@ -0,0 +1,33 @@
1
+ name: Quality
2
+ on:
3
+ push:
4
+ branches: [ main ]
5
+ pull_request:
6
+ branches: [ main ]
7
+ permissions:
8
+ contents: read
9
+ jobs:
10
+ quality:
11
+ # One name, not a matrix: ruff and mypy read their target version from
12
+ # pyproject.toml, so running them on four interpreters would repeat the
13
+ # same answer four times.
14
+ name: Quality
15
+ runs-on: ubuntu-26.04
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v7
20
+ with:
21
+ python-version: "3.12"
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install -e ".[dev]" # NOSONAR - trusted internal project, no external setup scripts
26
+ # `tools/` is in here because it was not, and that is where
27
+ # security_exceptions.py lives: the script that refuses a build over an
28
+ # unexplained alert had never been seen by the linter that gates the build.
29
+ # Found on 2026-10-02 by running ruff over the whole tree by hand.
30
+ - name: Lint (ruff)
31
+ run: ruff check releaseradar tests tools
32
+ - name: Type check (mypy)
33
+ run: mypy releaseradar
@@ -0,0 +1,47 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ name: Create GitHub Release
14
+ runs-on: ubuntu-26.04
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Extract version from tag
22
+ id: version
23
+ run: |
24
+ VERSION=${GITHUB_REF#refs/tags/v}
25
+ echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
26
+ echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
27
+
28
+ - name: Extract changelog for this version
29
+ id: changelog
30
+ run: |
31
+ VERSION=${{ steps.version.outputs.VERSION }}
32
+ NOTES=$(awk "/## \[$VERSION\]/{flag=1; next} /## \[/{flag=0} flag" CHANGELOG.md)
33
+ if [ -z "$NOTES" ]; then
34
+ NOTES="Release $VERSION"
35
+ fi
36
+ echo "NOTES<<EOF" >> $GITHUB_OUTPUT
37
+ echo "$NOTES" >> $GITHUB_OUTPUT
38
+ echo "EOF" >> $GITHUB_OUTPUT
39
+
40
+ - name: Create GitHub Release
41
+ uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
42
+ with:
43
+ tag_name: ${{ steps.version.outputs.TAG }}
44
+ name: ReleaseRadar ${{ steps.version.outputs.VERSION }}
45
+ body: ${{ steps.changelog.outputs.NOTES }}
46
+ draft: false
47
+ prerelease: false
@@ -0,0 +1,120 @@
1
+ name: Security Posture
2
+ # The gate that refuses. Every other security workflow here reports: snyk.yml
3
+ # carries `continue-on-error` so a pull request is not blocked by findings that
4
+ # were already there, CodeQL and Docker Scout upload SARIF, and SonarCloud
5
+ # evaluates its quality gate after its job has finished succeeding. On 2026-09-28
6
+ # all of them were green while thirteen high-severity alerts were open and the
7
+ # Sonar gate was red.
8
+ #
9
+ # This job reads what they published — GitHub's code scanning alerts, which is
10
+ # where all four end up — and fails when a blocking finding has nobody's name
11
+ # against it. `SECURITY-EXCEPTIONS.toml` holds the accepted ones, each with a
12
+ # reason and a review date; tools/security_exceptions.py is the rule, and its
13
+ # tests are in tests/test_security_exceptions.py.
14
+ on:
15
+ push:
16
+ branches: [ main ]
17
+ pull_request:
18
+ branches: [ main ]
19
+ # Weekly, because an exception expires by the calendar and not by a commit: a
20
+ # review date that passes while nobody pushes has to fail on its own.
21
+ schedule:
22
+ - cron: '0 6 * * 1'
23
+ workflow_dispatch:
24
+
25
+ permissions:
26
+ contents: read
27
+ # Reading the alerts the other workflows uploaded.
28
+ security-events: read
29
+
30
+ jobs:
31
+ posture:
32
+ name: Security Posture
33
+ runs-on: ubuntu-26.04
34
+ steps:
35
+ - name: Checkout
36
+ uses: actions/checkout@v7
37
+
38
+ - name: Set up Python
39
+ uses: actions/setup-python@v7
40
+ with:
41
+ python-version: "3.11"
42
+
43
+ # The gate reads what the scanners published, so it has to run after they
44
+ # have spoken about *this* commit. On 5fa1774 it did not: it read the alerts
45
+ # while CodeQL was still analysing, reported the finding that commit had
46
+ # just fixed, and failed a build whose product was clean.
47
+ #
48
+ # Waiting on the runs of this SHA rather than sleeping a fixed time: the
49
+ # analyses take what they take, and a sleep is either too short or wasted.
50
+ # A timeout says so and checks nothing, because "the scanners did not
51
+ # finish" is not "there is nothing wrong".
52
+ - name: Wait for this commit's analyses
53
+ id: wait
54
+ env:
55
+ GH_TOKEN: ${{ github.token }}
56
+ UPLOADERS: CodeQL,Snyk,Docker Scout CVE Scan,Trivy Security Scan
57
+ run: |
58
+ # The counting is done in Python and not in `--jq`: gh's own jq takes a
59
+ # filter and not `--arg`, so the commit and the names would have to be
60
+ # pasted into the expression.
61
+ deadline=$(( SECONDS + 1200 ))
62
+ while [ "$SECONDS" -lt "$deadline" ]; do
63
+ gh run list --limit 40 --json name,status,headSha > runs.json
64
+ pending=$(python - <<'COUNT'
65
+ import json, os
66
+ wanted = set(os.environ["UPLOADERS"].split(","))
67
+ sha = os.environ["GITHUB_SHA"]
68
+ runs = json.load(open("runs.json", encoding="utf-8"))
69
+ print(sum(1 for r in runs
70
+ if r["headSha"] == sha and r["name"] in wanted
71
+ and r["status"] != "completed"))
72
+ COUNT
73
+ )
74
+ if [ "$pending" = "0" ]; then
75
+ echo "ready=true" >> "$GITHUB_OUTPUT"
76
+ exit 0
77
+ fi
78
+ echo "waiting for $pending analysis run(s) of $GITHUB_SHA"
79
+ sleep 30
80
+ done
81
+ echo "ready=false" >> "$GITHUB_OUTPUT"
82
+
83
+ - name: Report that the analyses did not finish
84
+ if: ${{ steps.wait.outputs.ready == 'false' }}
85
+ run: |
86
+ echo "::notice title=Posture not evaluated::The scanners of this commit did not finish within twenty minutes, so their alerts describe an earlier commit. Nothing was checked — and that is not the same as nothing being wrong."
87
+
88
+ # A fork pull request gets a token that cannot read code scanning alerts,
89
+ # and an empty list would read as a clean product. The job says so and
90
+ # stops instead of passing on no information.
91
+ - name: Collect the open alerts
92
+ if: ${{ steps.wait.outputs.ready == 'true' }}
93
+ id: alerts
94
+ env:
95
+ GH_TOKEN: ${{ github.token }}
96
+ run: |
97
+ if gh api "repos/${GITHUB_REPOSITORY}/code-scanning/alerts?state=open&per_page=100" \
98
+ > alerts.json 2>api-error.txt; then
99
+ echo "count=$(python -c 'import json;print(len(json.load(open("alerts.json"))))')" >> "$GITHUB_OUTPUT"
100
+ else
101
+ echo "readable=false" >> "$GITHUB_OUTPUT"
102
+ cat api-error.txt
103
+ fi
104
+ # The closed ones tell a finding that went quiet apart from an entry
105
+ # that never described anything. A scanner that drops a finding for one
106
+ # run must not fail the build — Docker Scout did exactly that to
107
+ # CVE-2026-82560 on 2026-09-29 and had already changed its mind back
108
+ # once. Best effort: without this file the tool falls back to the
109
+ # stricter reading, which is the safe direction.
110
+ gh api "repos/${GITHUB_REPOSITORY}/code-scanning/alerts?state=closed&per_page=100" \
111
+ > closed.json 2>/dev/null || echo "[]" > closed.json
112
+
113
+ - name: Report that the alerts could not be read
114
+ if: ${{ steps.alerts.outputs.readable == 'false' }}
115
+ run: |
116
+ echo "::notice title=Posture not evaluated::This run cannot read code scanning alerts (a fork pull request, or the feature is off). Nothing was checked — and that is not the same as nothing being wrong."
117
+
118
+ - name: Every blocking alert is explained, and every exception is in date
119
+ if: ${{ steps.wait.outputs.ready == 'true' && steps.alerts.outputs.readable != 'false' }}
120
+ run: python tools/security_exceptions.py alerts.json closed.json