From 53d937505b0e400221a7f341fc3d42f1e460928b Mon Sep 17 00:00:00 2001 From: stacknil Date: Tue, 30 Jun 2026 22:06:05 +0800 Subject: [PATCH] feat(sbom): prepare v1.0 release candidate evidence --- .github/workflows/sbom-diff-and-risk-ci.yml | 22 +- README.md | 10 +- scripts/validate-reviewer-routes.py | 26 + tools/sbom-diff-and-risk/README.md | 15 +- .../RELEASE_NOTES_v1.0-rc.1.md | 56 ++ .../docs/github-actions-consumer-example.md | 6 +- .../docs/policy-warning-reviewer-case.md | 94 +++ .../sbom-diff-and-risk/docs/reviewer-brief.md | 4 +- .../docs/reviewer-evidence-pack.md | 29 +- .../sbom-diff-and-risk/docs/reviewer-path.md | 3 + .../github-actions-policy-consumer.yml | 6 +- .../examples/sample-provenance-report.sarif | 4 +- .../examples/sample-sarif.sarif | 600 +++++++++--------- .../examples/sample-scorecard-report.sarif | 4 +- tools/sbom-diff-and-risk/pyproject.toml | 2 +- .../src/sbom_diff_risk/__init__.py | 2 +- .../test_policy_warning_reviewer_case.py | 41 ++ .../tests/test_release_workflow.py | 24 + 18 files changed, 608 insertions(+), 340 deletions(-) create mode 100644 tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0-rc.1.md create mode 100644 tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md create mode 100644 tools/sbom-diff-and-risk/tests/test_policy_warning_reviewer_case.py create mode 100644 tools/sbom-diff-and-risk/tests/test_release_workflow.py diff --git a/.github/workflows/sbom-diff-and-risk-ci.yml b/.github/workflows/sbom-diff-and-risk-ci.yml index 1149e19..fab26ae 100644 --- a/.github/workflows/sbom-diff-and-risk-ci.yml +++ b/.github/workflows/sbom-diff-and-risk-ci.yml @@ -170,6 +170,7 @@ jobs: assets=( $(printf '%s\n' "${assets[@]}" | LC_ALL=C sort) ) unset IFS checksum_manifest="release-assets/${SBOM_DIFF_RISK_CHECKSUM_MANIFEST}" + notes_file="tools/sbom-diff-and-risk/RELEASE_NOTES_${RELEASE_TAG}.md" if [ "${#assets[@]}" -ne 2 ]; then echo "Expected exactly one wheel and one source distribution in release-assets/." >&2 @@ -188,6 +189,19 @@ jobs: assets+=( "${checksum_manifest}" ) title="${RELEASE_TITLE_PREFIX} ${RELEASE_TAG}" + create_args=(--draft --verify-tag --title "${title}") + edit_args=(--draft=false --title "${title}") + notes_args=(--notes "Release assets for ${RELEASE_TAG}. See docs/release-provenance.md for provenance verification guidance.") + + if [[ "${RELEASE_TAG}" == *"rc"* ]]; then + create_args+=(--prerelease --latest=false) + edit_args+=(--prerelease) + fi + + if [ -f "${notes_file}" ]; then + notes_args=(--notes-file "${notes_file}") + edit_args+=(--notes-file "${notes_file}") + fi if gh release view "${RELEASE_TAG}" --repo "${GH_REPO}" >/dev/null 2>&1; then is_draft="$(gh release view "${RELEASE_TAG}" --repo "${GH_REPO}" --json isDraft -q .isDraft)" @@ -198,11 +212,9 @@ jobs: else gh release create "${RELEASE_TAG}" \ --repo "${GH_REPO}" \ - --draft \ - --verify-tag \ - --title "${title}" \ - --notes "Release assets for ${RELEASE_TAG}. See docs/release-provenance.md for provenance verification guidance." + "${create_args[@]}" \ + "${notes_args[@]}" fi gh release upload "${RELEASE_TAG}" "${assets[@]}" --repo "${GH_REPO}" - gh release edit "${RELEASE_TAG}" --repo "${GH_REPO}" --draft=false --title "${title}" + gh release edit "${RELEASE_TAG}" --repo "${GH_REPO}" "${edit_args[@]}" diff --git a/README.md b/README.md index e9ff7e4..6410668 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Project: [`sbom-diff-and-risk`](tools/sbom-diff-and-risk/README.md) Status: -Released at `v0.9.0`. +Released at `v1.0-rc.1` as a release candidate. What to review: Deterministic SBOM/dependency diffing, JSON/Markdown/SARIF output, local policy @@ -86,7 +86,7 @@ Useful entry points: - [Reviewer path](tools/sbom-diff-and-risk/docs/reviewer-path.md) - [Reviewer brief](tools/sbom-diff-and-risk/docs/reviewer-brief.md) - [Reviewer evidence pack](tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md) -- [v0.9.0 release notes][release-notes-v090] +- [v1.0-rc.1 release notes][release-notes-v10rc1] - [Examples](tools/sbom-diff-and-risk/examples/) Project: @@ -212,9 +212,9 @@ the review question: ## Status -- Current flagship release: `sbom-diff-and-risk` `v0.9.0` -- GitHub Release assets: available for `v0.9.0` +- Current flagship release candidate: `sbom-diff-and-risk` `v1.0-rc.1` +- GitHub Release assets: available for `v1.0-rc.1` - TestPyPI Trusted Publishing dry-run: completed - Production PyPI publishing: intentionally deferred -[release-notes-v090]: tools/sbom-diff-and-risk/RELEASE_NOTES_v0.9.0.md +[release-notes-v10rc1]: tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0-rc.1.md diff --git a/scripts/validate-reviewer-routes.py b/scripts/validate-reviewer-routes.py index 2c7cc46..71076bd 100644 --- a/scripts/validate-reviewer-routes.py +++ b/scripts/validate-reviewer-routes.py @@ -18,6 +18,7 @@ Path("docs/risk-model-boundary.md"), Path("tools/sbom-diff-and-risk/docs/report-schema.md"), Path("tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md"), + Path("tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md"), Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"), Path("projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md"), Path("projects/precipitation-anomaly-diagnostics-lab/docs/reviewer-path.md"), @@ -86,10 +87,20 @@ "tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md", "tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml", }, + Path("tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md"): { + "docs/risk-model-boundary.md", + "tools/sbom-diff-and-risk/docs/policy-decision-explainability.md", + "tools/sbom-diff-and-risk/examples/cdx_after.json", + "tools/sbom-diff-and-risk/examples/cdx_before.json", + "tools/sbom-diff-and-risk/examples/policy-minimal.yml", + "tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json", + "tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md", + }, Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): { ".github/workflows/reviewer-route-contract-ci.yml", "docs/risk-model-boundary.md", "scripts/validate-reviewer-routes.py", + "tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md", "tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml", "tools/sbom-diff-and-risk/docs/reviewer-brief.md", "tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md", @@ -225,6 +236,21 @@ "not a CVE scanner", "not a dependency safety oracle", ), + Path("tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md"): ( + "Policy warning reviewer case", + "how does a dependency diff produce a local policy warning?", + "summary.policy.status", + "warn", + "policy_matched", + "new_package", + "urllib3", + "risk_finding_matched_policy_rule", + "severity_source", + "warn_on", + "not a package safety verdict", + "any CVE result", + "any malware verdict", + ), Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): ( "Artifact evidence map", "Reviewer route contract", diff --git a/tools/sbom-diff-and-risk/README.md b/tools/sbom-diff-and-risk/README.md index dd2693b..5240ff8 100644 --- a/tools/sbom-diff-and-risk/README.md +++ b/tools/sbom-diff-and-risk/README.md @@ -1,10 +1,11 @@ # sbom-diff-and-risk -v0.9.0 is the policy JSON sidecar and consumer integration usability release. -It adds optional `--policy-json PATH` output, checked-in policy sidecar -examples, and copyable GitHub Actions consumer guidance. It keeps dependency -analysis local and deterministic by default, preserves the completed TestPyPI -dry-run story, and keeps production PyPI publishing intentionally deferred. +v1.0-rc.1 is the Policy Evidence release candidate. It fixes the reviewer path +for policy decision examples, evidence-confidence labels, a one-page policy +warning case, and copyable GitHub Actions consumer guidance. It keeps +dependency analysis local and deterministic by default, preserves the completed +TestPyPI dry-run story, and keeps production PyPI publishing intentionally +deferred. `sbom-diff-and-risk` is a local, deterministic CLI for comparing two SBOMs or dependency manifests and producing JSON plus Markdown reports. @@ -29,6 +30,9 @@ explainability fields, see [docs/policy-decision-explainability.md](docs/policy-decision-explainability.md). For CI consumption of policy decision fields, see [docs/policy-decision-ci-cookbook.md](docs/policy-decision-ci-cookbook.md). +For a fixed one-page case showing how a dependency diff triggers a policy +warning, see +[docs/policy-warning-reviewer-case.md](docs/policy-warning-reviewer-case.md). For CI consumption of summary-only output, see [docs/summary-json-ci-cookbook.md](docs/summary-json-ci-cookbook.md). For a consumer-facing GitHub Actions example, see @@ -319,6 +323,7 @@ The [examples/](examples/) directory includes: - sample policy-fail reports at [sample-policy-fail-report.json](examples/sample-policy-fail-report.json) and [sample-policy-fail-report.md](examples/sample-policy-fail-report.md) - a sample policy-only sidecar at [sample-policy.json](examples/sample-policy.json) - compact policy-decision examples at [examples/policy-decisions](examples/policy-decisions/README.md) +- a one-page policy warning reviewer case at [docs/policy-warning-reviewer-case.md](docs/policy-warning-reviewer-case.md) - a sample SARIF export at [sample-sarif.sarif](examples/sample-sarif.sarif) - provenance-aware sample reports at [sample-provenance-report.json](examples/sample-provenance-report.json), [sample-provenance-report.md](examples/sample-provenance-report.md), and [sample-provenance-report.sarif](examples/sample-provenance-report.sarif) - Scorecard-aware sample reports at [sample-scorecard-report.json](examples/sample-scorecard-report.json), [sample-scorecard-report.md](examples/sample-scorecard-report.md), and [sample-scorecard-report.sarif](examples/sample-scorecard-report.sarif) diff --git a/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0-rc.1.md b/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0-rc.1.md new file mode 100644 index 0000000..a53f780 --- /dev/null +++ b/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0-rc.1.md @@ -0,0 +1,56 @@ +# sbom-diff-and-risk v1.0-rc.1 + +`v1.0-rc.1` is the Policy Evidence release candidate. + +The Python package metadata version for this tag is `1.0rc1`, which is the +PEP 440 form used in the wheel and source distribution filenames. + +## Theme + +Reviewer-stable policy evidence without expanding the tool's claims. + +This release candidate turns the post-`v0.9.0` policy work into a tighter +review surface: fixed policy decision examples, explicit evidence-confidence +labels, a risk-model boundary, and a minimal CI consumer path. + +## Highlights + +- Added policy decision examples for `pass`, `warn`, `fail`, and + consumer-side `needs-review`. +- Added `summary.evidence_confidence` and top-level `evidence_confidence` + labels for `local_manifest_only`, `sbom_present`, `policy_matched`, + `enrichment_recorded`, and `provenance_recorded`. +- Added a one-page policy warning reviewer case that traces an added + dependency from diff input to local policy warning. +- Strengthened the risk-model boundary with explicit non-claims: not a CVE + scanner, not a malware scanner, and not a package safety verdict engine. +- Added a minimal GitHub Actions consumer workflow that runs the tool, uploads + `policy.json`, and fails or passes from the local policy result. +- Added repository scope and scientific-computing background notes to keep the + repository from widening beyond the flagship SBOM release surface. + +## Compatibility and boundaries + +- This is a release candidate, not the final `v1.0.0`. +- Production PyPI publishing remains intentionally deferred. +- The GitHub Release assets are the expected distribution surface for this rc. +- Default analysis remains local-file based and deterministic. +- No default network enrichment was added. +- No CVE lookup, advisory resolution, malware scanning, or package safety + verdict was added. +- Policy warnings and failures remain local policy decisions for review. + +## Release evidence + +The tag-gated GitHub Actions workflow builds the wheel and source distribution, +generates a SHA256 checksum manifest, records workflow artifact attestations, +and publishes the same built files as GitHub Release assets. + +Expected assets: + +- `sbom_diff_and_risk-1.0rc1-py3-none-any.whl` +- `sbom_diff_and_risk-1.0rc1.tar.gz` +- `sbom-diff-and-risk-SHA256SUMS.txt` + +Use `docs/verification.md`, `docs/release-provenance.md`, and +`docs/self-provenance.md` for the correct verification path. diff --git a/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md b/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md index a2dc5d3..cbbebe4 100644 --- a/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md +++ b/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md @@ -47,15 +47,15 @@ jobs: GH_TOKEN: ${{ github.token }} run: | mkdir -p .tooling/sbom-diff-risk - gh release download v0.9.0 \ + gh release download v1.0-rc.1 \ --repo stacknil/scientific-computing-toolkit \ - --pattern "sbom_diff_and_risk-0.9.0-py3-none-any.whl" \ + --pattern "sbom_diff_and_risk-1.0rc1-py3-none-any.whl" \ --dir .tooling/sbom-diff-risk - name: Install sbom-diff-risk run: | python -m pip install \ - .tooling/sbom-diff-risk/sbom_diff_and_risk-0.9.0-py3-none-any.whl + .tooling/sbom-diff-risk/sbom_diff_and_risk-1.0rc1-py3-none-any.whl - name: Run dependency policy id: compare diff --git a/tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md b/tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md new file mode 100644 index 0000000..9d3b866 --- /dev/null +++ b/tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md @@ -0,0 +1,94 @@ +# Policy warning reviewer case + +This one-page case fixes a single reviewer question: how does a dependency diff +produce a local policy warning? + +It uses the checked-in CycloneDX example pair and the minimal policy. No +network enrichment, CVE lookup, malware scan, or package safety verdict is +involved. + +## Fixed input + +Run from `tools/sbom-diff-and-risk`: + +```powershell +sbom-diff-risk compare ` + --before examples/cdx_before.json ` + --after examples/cdx_after.json ` + --policy examples/policy-minimal.yml ` + --out-json outputs/policy-warn-report.json ` + --out-md outputs/policy-warn-report.md +``` + +Reference inputs and policy: + +- [`examples/cdx_before.json`](../examples/cdx_before.json) +- [`examples/cdx_after.json`](../examples/cdx_after.json) +- [`examples/policy-minimal.yml`](../examples/policy-minimal.yml) + +The minimal policy is intentionally small: + +```yaml +version: 1 +block_on: + - unknown_license +warn_on: + - new_package +``` + +## Fixed output + +The checked-in reference outputs are: + +- [`examples/sample-policy-warn-report.json`](../examples/sample-policy-warn-report.json) +- [`examples/sample-policy-warn-report.md`](../examples/sample-policy-warn-report.md) + +The fixed JSON facts for this case are: + +| Field | Value | +| --- | --- | +| `summary.added` | `1` | +| `summary.changed` | `1` | +| `summary.evidence_confidence` | `policy_matched` | +| `summary.policy.status` | `warn` | +| `summary.policy.blocking` | `0` | +| `summary.policy.warning` | `1` | +| `warning_findings[0].policy_rule` | `new_package` | +| `warning_findings[0].component_name` | `urllib3` | +| `warning_findings[0].decision_reason` | `risk_finding_matched_policy_rule` | +| `warning_findings[0].severity_source` | `warn_on` | +| `warning_findings[0].observed_value` | `new_package` | + +## Explanation + +The after input contains `urllib3` `2.2.1`, which is not present in the before +input. The local risk model classifies that component as `new_package`. + +`examples/policy-minimal.yml` maps `new_package` to `warn_on`, so policy +evaluation adds one warning finding for `urllib3`. The tool exits successfully +because the warning does not become a blocking policy violation. + +The `requests` version change is still visible in the report, but it is not the +source of this policy warning. It receives `version_change_unclassified` and +`not_evaluated` findings in offline mode, and the minimal policy does not warn +or block on either rule. + +## Fixed boundary + +This case proves that local policy matching can explain why a dependency diff +triggered a warning. + +It does not prove: + +- `urllib3` is unsafe +- `requests` is safe +- any CVE result +- any malware verdict +- not a package safety verdict +- current PyPI package truth +- current repository reputation + +For field-level policy metadata, see +[policy-decision-explainability.md](policy-decision-explainability.md). For +the risk inputs and non-claims, see +[risk-model-boundary.md](../../../docs/risk-model-boundary.md). diff --git a/tools/sbom-diff-and-risk/docs/reviewer-brief.md b/tools/sbom-diff-and-risk/docs/reviewer-brief.md index 2a72fc5..6c2e5dc 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-brief.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-brief.md @@ -4,7 +4,7 @@ `sbom-diff-and-risk` is a local CLI for comparing two SBOMs or dependency manifests and producing deterministic review artifacts: JSON, Markdown, and SARIF. It is built for conservative supply-chain review, not for vulnerability scanning or package reputation scoring. -Current released version: `v0.9.0`. +Current released version: `v1.0-rc.1` release candidate. For the shortest ordered review route, use [reviewer-path.md](reviewer-path.md). @@ -32,6 +32,7 @@ Dependency review often needs evidence that is stable enough for code review, CI | How can a reviewer reproduce the core evidence? | [reviewer-evidence-pack.md](reviewer-evidence-pack.md) for demo, release, TestPyPI, and SARIF verification paths. | | What is the stable JSON shape? | [report-schema.md](report-schema.md) documents the machine-readable report structure and `summary` contract. | | How are policy findings explained? | [policy-decision-explainability.md](policy-decision-explainability.md) documents the policy decision metadata in JSON reports. | +| How does one dependency diff trigger a policy warning? | [policy-warning-reviewer-case.md](policy-warning-reviewer-case.md) fixes the input, output, explanation, and boundary for a `new_package` warning. | | Can CI consume compact policy decisions? | [policy-decision-ci-cookbook.md](policy-decision-ci-cookbook.md), [examples/sample-policy.json](../examples/sample-policy.json), and [examples/github-actions-policy-consumer.yml](../examples/github-actions-policy-consumer.yml) show the policy sidecar path. | | Are default runs offline? | CLI docs, tests for no-enrichment behavior, and explicit enrichment flags. | | Can code scanning consume the output? | `docs/github-code-scanning.md` and `examples/sample-sarif.sarif`. | @@ -64,4 +65,3 @@ Dependency review often needs evidence that is stable enough for code review, CI ## Resume / application wording Built `sbom-diff-and-risk`, a deterministic SBOM and dependency diff CLI that produces JSON, Markdown, and SARIF review artifacts; supports local policy checks and optional provenance/Scorecard evidence; and documents a release verification story covering GitHub artifact attestations, GitHub Release assets, TestPyPI Trusted Publishing validation, and intentionally deferred production PyPI publishing. - diff --git a/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md b/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md index bdcf028..0653024 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md @@ -14,7 +14,7 @@ workflow templates. `sbom-diff-and-risk` is a local-first deterministic CLI for comparing SBOMs and dependency manifests. It is designed to produce stable review evidence for dependency changes. -Current released version: `v0.9.0`. +Current released version: `v1.0-rc.1` release candidate. Core identity: @@ -131,6 +131,12 @@ For policy finding interpretation, see documents the policy decision metadata used to explain local blocks, warnings, and suppressions. +For a fixed one-page policy warning case, see +[policy-warning-reviewer-case.md](policy-warning-reviewer-case.md). It traces +the checked-in CycloneDX example pair and `policy-minimal.yml` from added +dependency to `new_package` warning without turning the warning into a package +safety verdict. + For CI job-summary examples that consume policy decision metadata, see [policy-decision-ci-cookbook.md](policy-decision-ci-cookbook.md). @@ -143,21 +149,23 @@ For CI dashboard, job-summary, and local-threshold examples that consume ## Release Verification Path -Start with the GitHub Release for the version under review. For `v0.9.0`, +Start with the GitHub Release for the version under review. For `v1.0-rc.1`, inspect the release and assets: ```powershell -gh release view v0.9.0 ` +gh release view v1.0-rc.1 ` --repo stacknil/scientific-computing-toolkit ` --json tagName,name,isDraft,isPrerelease,assets,url ``` Expected release assets: -- `sbom_diff_and_risk-0.9.0-py3-none-any.whl` -- `sbom_diff_and_risk-0.9.0.tar.gz` +- `sbom_diff_and_risk-1.0rc1-py3-none-any.whl` +- `sbom_diff_and_risk-1.0rc1.tar.gz` - `sbom-diff-and-risk-SHA256SUMS.txt` +For this rc, `isPrerelease` should be `true`. + The checksum manifest checks local downloaded distribution bytes before or alongside provenance verification: ```powershell @@ -186,13 +194,13 @@ For workflow-built artifacts downloaded from a trusted workflow run, verify artifact attestations with the signer workflow: ```powershell -gh attestation verify path/to/sbom_diff_and_risk-0.9.0-py3-none-any.whl ` +gh attestation verify path/to/sbom_diff_and_risk-1.0rc1-py3-none-any.whl ` --repo stacknil/scientific-computing-toolkit ` --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml ``` ```powershell -gh attestation verify path/to/sbom_diff_and_risk-0.9.0.tar.gz ` +gh attestation verify path/to/sbom_diff_and_risk-1.0rc1.tar.gz ` --repo stacknil/scientific-computing-toolkit ` --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml ``` @@ -202,15 +210,15 @@ releases. Use them only when the repository release is immutable and GitHub has generated release attestations: ```powershell -gh release view v0.9.0 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url +gh release view v1.0-rc.1 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url ``` If `isImmutable` is true, release verification can check the release record and downloaded release assets: ```powershell -gh release verify v0.9.0 --repo stacknil/scientific-computing-toolkit -gh release verify-asset v0.9.0 path/to/sbom_diff_and_risk-0.9.0-py3-none-any.whl --repo stacknil/scientific-computing-toolkit +gh release verify v1.0-rc.1 --repo stacknil/scientific-computing-toolkit +gh release verify-asset v1.0-rc.1 path/to/sbom_diff_and_risk-1.0rc1-py3-none-any.whl --repo stacknil/scientific-computing-toolkit ``` If `isImmutable` is false, use the workflow artifact attestation path as the primary artifact verification story. @@ -275,4 +283,3 @@ Avoid overclaiming: - Can I verify release/distribution evidence? Read `verification.md`, `self-provenance.md`, and `release-provenance.md`. - Can I distinguish TestPyPI from production PyPI? Read `pypi-trusted-publishing-readiness.md` and `pypi-production-publishing-decision.md`. - Can I state the non-claims? No CVE scanner, no reputation oracle, no dependency safety verdicts, no production PyPI package yet. - diff --git a/tools/sbom-diff-and-risk/docs/reviewer-path.md b/tools/sbom-diff-and-risk/docs/reviewer-path.md index c03857e..c378c29 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-path.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-path.md @@ -11,6 +11,7 @@ where to find it, and what it does not prove. | What is this tool? | [30-second orientation](#30-second-orientation) | You can state the tool's scope and non-claims. | | What artifacts does it produce? | [5-minute artifact review](#5-minute-artifact-review) | You can point to JSON, summary, policy, Markdown, and SARIF examples. | | Can the examples be reproduced locally? | [15-minute reproduction check](#15-minute-reproduction-check) | `regenerate-example-artifacts.py --check` passes without enrichment. | +| How does a dependency diff trigger a policy warning? | [Policy warning reviewer case](policy-warning-reviewer-case.md) | You can trace `urllib3` from added dependency to `new_package` warning without overclaiming. | | Can the released tool artifacts be verified? | [Release evidence](#release-evidence) | You can choose the correct GitHub release, checksum, or attestation path. | | Are the reviewer routes still valid? | [Reviewer route contract](#reviewer-route-contract) | `python scripts/validate-reviewer-routes.py` passes from the repository root. | | What can I safely say in review? | [Reviewer outcome statements](#reviewer-outcome-statements) | Your summary separates verified evidence from non-claims. | @@ -90,6 +91,7 @@ consumer workflow templates. They answer different review questions. | --- | --- | --- | --- | --- | | Default diff output | [sample-report.json](../examples/sample-report.json), [sample-summary.json](../examples/sample-summary.json), [sample-report.md](../examples/sample-report.md) | `scripts/regenerate-example-artifacts.py` with the CycloneDX example inputs | No network | Verify the default report, compact summary, and Markdown shapes. | | Policy output | [sample-policy-warn-report.json](../examples/sample-policy-warn-report.json), [sample-policy-fail-report.json](../examples/sample-policy-fail-report.json), [sample-policy.json](../examples/sample-policy.json) | `scripts/regenerate-example-artifacts.py` with local policy files | No network | Review warning, blocking, and policy-only sidecar semantics. | +| Fixed policy warning case | [policy-warning-reviewer-case.md](policy-warning-reviewer-case.md) | Checked-in CycloneDX inputs, `policy-minimal.yml`, and sample policy-warn outputs | No network | Trace one added dependency from diff to local `new_package` policy warning with fixed input, output, explanation, and boundary. | | Policy decision examples | [policy-decisions](../examples/policy-decisions/README.md) | Checked-in consumer examples | No network | Review `pass`, `warn`, `fail`, and consumer-side `needs-review` interpretations without treating them as dependency safety verdicts. | | Requirements output | [sample-requirements-report.json](../examples/sample-requirements-report.json), [sample-requirements-report.md](../examples/sample-requirements-report.md) | `scripts/regenerate-example-artifacts.py --only requirements` | No network | Verify the `requirements.txt` parser path and report shape. | | Strict-policy SARIF | [sample-sarif.sarif](../examples/sample-sarif.sarif) | `scripts/regenerate-example-artifacts.py --only sarif` with normalized source root | No network | Inspect conservative code-scanning output for selected findings. | @@ -224,6 +226,7 @@ Use [reviewer-evidence-pack.md](reviewer-evidence-pack.md) for the full reproducible checklist, including: - local demo commands +- fixed one-page policy warning case - release asset inspection - checksum verification - artifact attestation verification diff --git a/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml b/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml index 0e92a4a..34e286b 100644 --- a/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml +++ b/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml @@ -29,15 +29,15 @@ jobs: GH_TOKEN: ${{ github.token }} run: | mkdir -p .tooling/sbom-diff-risk - gh release download v0.9.0 \ + gh release download v1.0-rc.1 \ --repo stacknil/scientific-computing-toolkit \ - --pattern "sbom_diff_and_risk-0.9.0-py3-none-any.whl" \ + --pattern "sbom_diff_and_risk-1.0rc1-py3-none-any.whl" \ --dir .tooling/sbom-diff-risk - name: Install sbom-diff-risk run: | python -m pip install \ - .tooling/sbom-diff-risk/sbom_diff_and_risk-0.9.0-py3-none-any.whl + .tooling/sbom-diff-risk/sbom_diff_and_risk-1.0rc1-py3-none-any.whl - name: Run dependency policy id: compare diff --git a/tools/sbom-diff-and-risk/examples/sample-provenance-report.sarif b/tools/sbom-diff-and-risk/examples/sample-provenance-report.sarif index 50e7e40..163ceb0 100644 --- a/tools/sbom-diff-and-risk/examples/sample-provenance-report.sarif +++ b/tools/sbom-diff-and-risk/examples/sample-provenance-report.sarif @@ -7,8 +7,8 @@ "driver": { "name": "sbom-diff-risk", "fullName": "sbom-diff-risk", - "version": "0.9.0", - "semanticVersion": "0.9.0", + "version": "1.0rc1", + "semanticVersion": "1.0rc1", "rules": [ { "id": "sdr.policy_violation.provenance_required", diff --git a/tools/sbom-diff-and-risk/examples/sample-sarif.sarif b/tools/sbom-diff-and-risk/examples/sample-sarif.sarif index 6f30da9..6dfc8d9 100644 --- a/tools/sbom-diff-and-risk/examples/sample-sarif.sarif +++ b/tools/sbom-diff-and-risk/examples/sample-sarif.sarif @@ -1,300 +1,300 @@ -{ - "$schema": "https://json.schemastore.org/sarif-2.1.0.json", - "version": "2.1.0", - "runs": [ - { - "tool": { - "driver": { - "name": "sbom-diff-risk", - "fullName": "sbom-diff-risk", - "version": "0.9.0", - "semanticVersion": "0.9.0", - "rules": [ - { - "id": "sdr.major_upgrade", - "name": "major_upgrade", - "shortDescription": { - "text": "Version change is a parseable SemVer major upgrade." - }, - "fullDescription": { - "text": "Version change is a parseable SemVer major upgrade." - }, - "defaultConfiguration": { - "level": "note" - }, - "properties": { - "tags": [ - "supply-chain", - "sbom" - ] - } - }, - { - "id": "sdr.policy_violation.allow_sources", - "name": "policy_violation.allow_sources", - "shortDescription": { - "text": "Policy violation: allow_sources" - }, - "fullDescription": { - "text": "Component source host was not present in the configured allow_sources list." - }, - "defaultConfiguration": { - "level": "error" - }, - "properties": { - "tags": [ - "supply-chain", - "policy" - ] - } - }, - { - "id": "sdr.policy_violation.max_added_packages", - "name": "policy_violation.max_added_packages", - "shortDescription": { - "text": "Policy violation: max_added_packages" - }, - "fullDescription": { - "text": "Added package count exceeded the configured deterministic threshold." - }, - "defaultConfiguration": { - "level": "error" - }, - "properties": { - "tags": [ - "supply-chain", - "policy" - ] - } - }, - { - "id": "sdr.suspicious_source", - "name": "suspicious_source", - "shortDescription": { - "text": "Source provenance is missing or points to a suspicious scheme, path, or host." - }, - "fullDescription": { - "text": "Source provenance is missing or points to a suspicious scheme, path, or host." - }, - "defaultConfiguration": { - "level": "warning" - }, - "properties": { - "tags": [ - "supply-chain", - "sbom" - ] - } - }, - { - "id": "sdr.unknown_license", - "name": "unknown_license", - "shortDescription": { - "text": "License metadata is missing, empty, UNKNOWN, or NOASSERTION." - }, - "fullDescription": { - "text": "License metadata is missing, empty, UNKNOWN, or NOASSERTION." - }, - "defaultConfiguration": { - "level": "warning" - }, - "properties": { - "tags": [ - "supply-chain", - "sbom" - ] - } - } - ] - } - }, - "artifacts": [ - { - "location": { - "uri": "examples/sarif_before.json", - "uriBaseId": "%SRCROOT%" - } - }, - { - "location": { - "uri": "examples/sarif_after.json", - "uriBaseId": "%SRCROOT%" - } - } - ], - "properties": { - "sbom_diff_risk": { - "result_limit": 5000, - "total_candidate_results": 5, - "emitted_results": 5, - "omitted_results": 0, - "truncated": false, - "prioritization": "error results first, then warning, then note; direct mapped findings before policy-only checks; stable rule priority and component key tie-breakers.", - "warning": null - } - }, - "results": [ - { - "ruleId": "sdr.suspicious_source", - "level": "error", - "message": { - "text": "Blocked by policy: mystery-lib 0.1.0 has suspicious or incomplete source provenance." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "examples/sarif_after.json", - "uriBaseId": "%SRCROOT%" - }, - "region": { - "startLine": 1 - } - } - } - ], - "partialFingerprints": { - "ruleId": "sdr.suspicious_source", - "componentKey": "purl:pkg:pypi/mystery-lib" - }, - "properties": { - "component_key": "purl:pkg:pypi/mystery-lib", - "component_name": "mystery-lib", - "finding_bucket": "suspicious_source", - "policy_blocking": true, - "result_kind": "risk_finding", - "blocking_rule_id": "suspicious_source" - } - }, - { - "ruleId": "sdr.unknown_license", - "level": "error", - "message": { - "text": "Blocked by policy: mystery-lib 0.1.0 has missing or unknown license metadata." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "examples/sarif_after.json", - "uriBaseId": "%SRCROOT%" - }, - "region": { - "startLine": 1 - } - } - } - ], - "partialFingerprints": { - "ruleId": "sdr.unknown_license", - "componentKey": "purl:pkg:pypi/mystery-lib" - }, - "properties": { - "component_key": "purl:pkg:pypi/mystery-lib", - "component_name": "mystery-lib", - "finding_bucket": "unknown_license", - "policy_blocking": true, - "result_kind": "risk_finding", - "blocking_rule_id": "unknown_license" - } - }, - { - "ruleId": "sdr.policy_violation.allow_sources", - "level": "error", - "message": { - "text": "mystery-lib: Source host 198.51.100.10 is not present in allow_sources." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "examples/sarif_after.json", - "uriBaseId": "%SRCROOT%" - }, - "region": { - "startLine": 1 - } - } - } - ], - "partialFingerprints": { - "ruleId": "sdr.policy_violation.allow_sources", - "componentKey": "purl:pkg:pypi/mystery-lib" - }, - "properties": { - "policy_rule_id": "allow_sources", - "component_key": "purl:pkg:pypi/mystery-lib", - "component_name": "mystery-lib", - "result_kind": "policy_violation" - } - }, - { - "ruleId": "sdr.policy_violation.max_added_packages", - "level": "error", - "message": { - "text": "Added package count 1 exceeds max_added_packages=0." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "examples/sarif_after.json", - "uriBaseId": "%SRCROOT%" - }, - "region": { - "startLine": 1 - } - } - } - ], - "partialFingerprints": { - "ruleId": "sdr.policy_violation.max_added_packages", - "componentKey": "global-policy-check" - }, - "properties": { - "policy_rule_id": "max_added_packages", - "component_key": null, - "component_name": null, - "result_kind": "policy_violation" - } - }, - { - "ruleId": "sdr.major_upgrade", - "level": "note", - "message": { - "text": "Version changed from 1.9.0 to 2.0.0 with a higher major version." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "examples/sarif_after.json", - "uriBaseId": "%SRCROOT%" - }, - "region": { - "startLine": 1 - } - } - } - ], - "partialFingerprints": { - "ruleId": "sdr.major_upgrade", - "componentKey": "purl:pkg:pypi/requests" - }, - "properties": { - "component_key": "purl:pkg:pypi/requests", - "component_name": "requests", - "finding_bucket": "major_upgrade", - "policy_blocking": false, - "result_kind": "risk_finding" - } - } - ], - "originalUriBaseIds": { - "%SRCROOT%": { - "uri": "file:///__PROJECT_ROOT__/" - } - } - } - ] -} +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "sbom-diff-risk", + "fullName": "sbom-diff-risk", + "version": "1.0rc1", + "semanticVersion": "1.0rc1", + "rules": [ + { + "id": "sdr.major_upgrade", + "name": "major_upgrade", + "shortDescription": { + "text": "Version change is a parseable SemVer major upgrade." + }, + "fullDescription": { + "text": "Version change is a parseable SemVer major upgrade." + }, + "defaultConfiguration": { + "level": "note" + }, + "properties": { + "tags": [ + "supply-chain", + "sbom" + ] + } + }, + { + "id": "sdr.policy_violation.allow_sources", + "name": "policy_violation.allow_sources", + "shortDescription": { + "text": "Policy violation: allow_sources" + }, + "fullDescription": { + "text": "Component source host was not present in the configured allow_sources list." + }, + "defaultConfiguration": { + "level": "error" + }, + "properties": { + "tags": [ + "supply-chain", + "policy" + ] + } + }, + { + "id": "sdr.policy_violation.max_added_packages", + "name": "policy_violation.max_added_packages", + "shortDescription": { + "text": "Policy violation: max_added_packages" + }, + "fullDescription": { + "text": "Added package count exceeded the configured deterministic threshold." + }, + "defaultConfiguration": { + "level": "error" + }, + "properties": { + "tags": [ + "supply-chain", + "policy" + ] + } + }, + { + "id": "sdr.suspicious_source", + "name": "suspicious_source", + "shortDescription": { + "text": "Source provenance is missing or points to a suspicious scheme, path, or host." + }, + "fullDescription": { + "text": "Source provenance is missing or points to a suspicious scheme, path, or host." + }, + "defaultConfiguration": { + "level": "warning" + }, + "properties": { + "tags": [ + "supply-chain", + "sbom" + ] + } + }, + { + "id": "sdr.unknown_license", + "name": "unknown_license", + "shortDescription": { + "text": "License metadata is missing, empty, UNKNOWN, or NOASSERTION." + }, + "fullDescription": { + "text": "License metadata is missing, empty, UNKNOWN, or NOASSERTION." + }, + "defaultConfiguration": { + "level": "warning" + }, + "properties": { + "tags": [ + "supply-chain", + "sbom" + ] + } + } + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "examples/sarif_before.json", + "uriBaseId": "%SRCROOT%" + } + }, + { + "location": { + "uri": "examples/sarif_after.json", + "uriBaseId": "%SRCROOT%" + } + } + ], + "properties": { + "sbom_diff_risk": { + "result_limit": 5000, + "total_candidate_results": 5, + "emitted_results": 5, + "omitted_results": 0, + "truncated": false, + "prioritization": "error results first, then warning, then note; direct mapped findings before policy-only checks; stable rule priority and component key tie-breakers.", + "warning": null + } + }, + "results": [ + { + "ruleId": "sdr.suspicious_source", + "level": "error", + "message": { + "text": "Blocked by policy: mystery-lib 0.1.0 has suspicious or incomplete source provenance." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "examples/sarif_after.json", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 1 + } + } + } + ], + "partialFingerprints": { + "ruleId": "sdr.suspicious_source", + "componentKey": "purl:pkg:pypi/mystery-lib" + }, + "properties": { + "component_key": "purl:pkg:pypi/mystery-lib", + "component_name": "mystery-lib", + "finding_bucket": "suspicious_source", + "policy_blocking": true, + "result_kind": "risk_finding", + "blocking_rule_id": "suspicious_source" + } + }, + { + "ruleId": "sdr.unknown_license", + "level": "error", + "message": { + "text": "Blocked by policy: mystery-lib 0.1.0 has missing or unknown license metadata." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "examples/sarif_after.json", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 1 + } + } + } + ], + "partialFingerprints": { + "ruleId": "sdr.unknown_license", + "componentKey": "purl:pkg:pypi/mystery-lib" + }, + "properties": { + "component_key": "purl:pkg:pypi/mystery-lib", + "component_name": "mystery-lib", + "finding_bucket": "unknown_license", + "policy_blocking": true, + "result_kind": "risk_finding", + "blocking_rule_id": "unknown_license" + } + }, + { + "ruleId": "sdr.policy_violation.allow_sources", + "level": "error", + "message": { + "text": "mystery-lib: Source host 198.51.100.10 is not present in allow_sources." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "examples/sarif_after.json", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 1 + } + } + } + ], + "partialFingerprints": { + "ruleId": "sdr.policy_violation.allow_sources", + "componentKey": "purl:pkg:pypi/mystery-lib" + }, + "properties": { + "policy_rule_id": "allow_sources", + "component_key": "purl:pkg:pypi/mystery-lib", + "component_name": "mystery-lib", + "result_kind": "policy_violation" + } + }, + { + "ruleId": "sdr.policy_violation.max_added_packages", + "level": "error", + "message": { + "text": "Added package count 1 exceeds max_added_packages=0." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "examples/sarif_after.json", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 1 + } + } + } + ], + "partialFingerprints": { + "ruleId": "sdr.policy_violation.max_added_packages", + "componentKey": "global-policy-check" + }, + "properties": { + "policy_rule_id": "max_added_packages", + "component_key": null, + "component_name": null, + "result_kind": "policy_violation" + } + }, + { + "ruleId": "sdr.major_upgrade", + "level": "note", + "message": { + "text": "Version changed from 1.9.0 to 2.0.0 with a higher major version." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "examples/sarif_after.json", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 1 + } + } + } + ], + "partialFingerprints": { + "ruleId": "sdr.major_upgrade", + "componentKey": "purl:pkg:pypi/requests" + }, + "properties": { + "component_key": "purl:pkg:pypi/requests", + "component_name": "requests", + "finding_bucket": "major_upgrade", + "policy_blocking": false, + "result_kind": "risk_finding" + } + } + ], + "originalUriBaseIds": { + "%SRCROOT%": { + "uri": "file:///__PROJECT_ROOT__/" + } + } + } + ] +} diff --git a/tools/sbom-diff-and-risk/examples/sample-scorecard-report.sarif b/tools/sbom-diff-and-risk/examples/sample-scorecard-report.sarif index 99c72e2..5f6de7b 100644 --- a/tools/sbom-diff-and-risk/examples/sample-scorecard-report.sarif +++ b/tools/sbom-diff-and-risk/examples/sample-scorecard-report.sarif @@ -7,8 +7,8 @@ "driver": { "name": "sbom-diff-risk", "fullName": "sbom-diff-risk", - "version": "0.9.0", - "semanticVersion": "0.9.0", + "version": "1.0rc1", + "semanticVersion": "1.0rc1", "rules": [ { "id": "sdr.policy_violation.scorecard_below_threshold", diff --git a/tools/sbom-diff-and-risk/pyproject.toml b/tools/sbom-diff-and-risk/pyproject.toml index ba178cf..86da491 100644 --- a/tools/sbom-diff-and-risk/pyproject.toml +++ b/tools/sbom-diff-and-risk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sbom-diff-and-risk" -version = "0.9.0" +version = "1.0rc1" description = "Deterministic SBOM diff CLI with heuristic risk reporting." readme = { file = "PYPI_DESCRIPTION.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/tools/sbom-diff-and-risk/src/sbom_diff_risk/__init__.py b/tools/sbom-diff-and-risk/src/sbom_diff_risk/__init__.py index 347cd8d..a7ed799 100644 --- a/tools/sbom-diff-and-risk/src/sbom_diff_risk/__init__.py +++ b/tools/sbom-diff-and-risk/src/sbom_diff_risk/__init__.py @@ -2,4 +2,4 @@ __all__ = ["__version__"] -__version__ = "0.9.0" +__version__ = "1.0rc1" diff --git a/tools/sbom-diff-and-risk/tests/test_policy_warning_reviewer_case.py b/tools/sbom-diff-and-risk/tests/test_policy_warning_reviewer_case.py new file mode 100644 index 0000000..661eb5f --- /dev/null +++ b/tools/sbom-diff-and-risk/tests/test_policy_warning_reviewer_case.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import json +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DOC_PATH = PROJECT_ROOT / "docs" / "policy-warning-reviewer-case.md" +WARN_REPORT = PROJECT_ROOT / "examples" / "sample-policy-warn-report.json" + + +def test_policy_warning_reviewer_case_matches_warn_fixture() -> None: + text = DOC_PATH.read_text(encoding="utf-8") + payload = json.loads(WARN_REPORT.read_text(encoding="utf-8")) + + warning = payload["warning_findings"][0] + + assert payload["summary"]["policy"] == { + "status": "warn", + "blocking": 0, + "warning": 1, + "suppressed": 0, + } + assert payload["summary"]["evidence_confidence"] == "policy_matched" + assert warning["policy_rule"] == "new_package" + assert warning["component_name"] == "urllib3" + assert warning["decision_reason"] == "risk_finding_matched_policy_rule" + assert warning["severity_source"] == "warn_on" + + for expected in ( + "summary.policy.status", + "policy_matched", + "new_package", + "urllib3", + "risk_finding_matched_policy_rule", + "warn_on", + "not a package safety verdict", + "any CVE result", + "any malware verdict", + ): + assert expected in text diff --git a/tools/sbom-diff-and-risk/tests/test_release_workflow.py b/tools/sbom-diff-and-risk/tests/test_release_workflow.py new file mode 100644 index 0000000..5ab1ea9 --- /dev/null +++ b/tools/sbom-diff-and-risk/tests/test_release_workflow.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[3] +WORKFLOW = REPO_ROOT / ".github" / "workflows" / "sbom-diff-and-risk-ci.yml" +RELEASE_NOTES = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "RELEASE_NOTES_v1.0-rc.1.md" + + +def test_release_workflow_marks_rc_tags_as_prereleases() -> None: + workflow = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8")) + publish_steps = workflow["jobs"]["publish-release-assets"]["steps"] + publish_script = publish_steps[-1]["run"] + + assert RELEASE_NOTES.is_file() + assert "RELEASE_NOTES_${RELEASE_TAG}.md" in publish_script + assert '"${RELEASE_TAG}" == *"rc"*' in publish_script + assert "create_args+=(--prerelease --latest=false)" in publish_script + assert "edit_args+=(--prerelease)" in publish_script + assert "notes_args=(--notes-file" in publish_script + assert "edit_args+=(--notes-file" in publish_script