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.
- releaseradar-2026.1/.gitattributes +3 -0
- releaseradar-2026.1/.github/scripts/wait_for_pypi.sh +36 -0
- releaseradar-2026.1/.github/workflows/bandit.yml +36 -0
- releaseradar-2026.1/.github/workflows/codeql.yml +35 -0
- releaseradar-2026.1/.github/workflows/docker-build-check.yml +83 -0
- releaseradar-2026.1/.github/workflows/docker-scout.yml +44 -0
- releaseradar-2026.1/.github/workflows/docker.yml +81 -0
- releaseradar-2026.1/.github/workflows/licenses.yml +33 -0
- releaseradar-2026.1/.github/workflows/mutation.yml +42 -0
- releaseradar-2026.1/.github/workflows/publish.yml +65 -0
- releaseradar-2026.1/.github/workflows/quality.yml +33 -0
- releaseradar-2026.1/.github/workflows/release.yml +47 -0
- releaseradar-2026.1/.github/workflows/security-posture.yml +120 -0
- releaseradar-2026.1/.github/workflows/snyk.yml +205 -0
- releaseradar-2026.1/.github/workflows/sonarcloud.yml +50 -0
- releaseradar-2026.1/.github/workflows/test.yml +77 -0
- releaseradar-2026.1/.github/workflows/trivy.yml +30 -0
- releaseradar-2026.1/.gitignore +109 -0
- releaseradar-2026.1/CHANGELOG.md +74 -0
- releaseradar-2026.1/Dockerfile +69 -0
- releaseradar-2026.1/LICENSE +21 -0
- releaseradar-2026.1/PKG-INFO +219 -0
- releaseradar-2026.1/README.md +157 -0
- releaseradar-2026.1/SECURITY-EXCEPTIONS.toml +80 -0
- releaseradar-2026.1/SECURITY.md +22 -0
- releaseradar-2026.1/pyproject.toml +110 -0
- releaseradar-2026.1/release.sh +108 -0
- releaseradar-2026.1/releaseradar/__init__.py +3 -0
- releaseradar-2026.1/releaseradar/cli.py +303 -0
- releaseradar-2026.1/releaseradar/db.py +311 -0
- releaseradar-2026.1/releaseradar/github.py +223 -0
- releaseradar-2026.1/releaseradar/mcp_server.py +174 -0
- releaseradar-2026.1/releaseradar/sync.py +129 -0
- releaseradar-2026.1/sonar-project.properties +20 -0
- releaseradar-2026.1/tests/__init__.py +0 -0
- releaseradar-2026.1/tests/conftest.py +68 -0
- releaseradar-2026.1/tests/docker/inspect.sh +87 -0
- releaseradar-2026.1/tests/docker/smoke.py +20 -0
- releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_issues_all.json +18 -0
- releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_pulls_all.json +18 -0
- releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_releases.json +18 -0
- releaseradar-2026.1/tests/fixtures/gh_codecov-mcp_repo.json +158 -0
- releaseradar-2026.1/tests/fixtures/gh_cookieradar_releases.json +738 -0
- releaseradar-2026.1/tests/fixtures/gh_missing_repo_404.json +22 -0
- releaseradar-2026.1/tests/fixtures/gh_missing_repo_issues_404.json +22 -0
- releaseradar-2026.1/tests/fixtures/gh_requests_issues_open_5.json +416 -0
- releaseradar-2026.1/tests/fixtures/gh_requests_pulls_open_5.json +1910 -0
- releaseradar-2026.1/tests/test_ci_contract.py +176 -0
- releaseradar-2026.1/tests/test_cli.py +220 -0
- releaseradar-2026.1/tests/test_db.py +264 -0
- releaseradar-2026.1/tests/test_declared_imports.py +151 -0
- releaseradar-2026.1/tests/test_docker_contract.py +128 -0
- releaseradar-2026.1/tests/test_github.py +200 -0
- releaseradar-2026.1/tests/test_mcp_server.py +181 -0
- releaseradar-2026.1/tests/test_project_metadata.py +57 -0
- releaseradar-2026.1/tests/test_release_script.py +402 -0
- releaseradar-2026.1/tests/test_security_exceptions.py +379 -0
- releaseradar-2026.1/tests/test_sync.py +205 -0
- releaseradar-2026.1/tests/test_version_contract.py +149 -0
- releaseradar-2026.1/tests/test_workflow_set.py +103 -0
- releaseradar-2026.1/tools/security_exceptions.py +457 -0
- releaseradar-2026.1/tools/snyk_ids.py +65 -0
- releaseradar-2026.1/tools/snyk_requirements.py +117 -0
|
@@ -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
|