Publish on a tag, prove the wheel delivers verdicts, and fix OPA being unusable in CI - #95
Merged
Merged
Conversation
Closes #60. The badge was removed in #59 because pepy.tech and shields.io return a red "not found" for the first day or two after a fresh PyPI release. The issue set the condition for putting it back: a non-red colour and a real count from curl https://img.shields.io/pepy/dt/aicertify.json which now returns {"message":"2.5k","color":"brightgreen"}. Added to the five READMEs the issue names, each with the alt text it specifies, directly after the PyPI badge. Also added to README-pypi.md, which the issue does not list: it is the badge row PyPI itself renders, and leaving it out would put a different row on the page a reader reaches from the badge.
GitHub has had v0.8.0 since 28 August while PyPI serves v0.7.3 from 14 May, so `pip install aicertify` does not install what this repository represents as the product. Publishing was a manual step, and manual steps get skipped. A tag now publishes, through PyPI Trusted Publishing, with no API token stored here. The publisher has to be configured once at https://pypi.org/manage/project/aicertify/settings/publishing/ against owner Principled-Evolution, repository aicertify, workflow release.yaml, environment pypi. Until that exists the publish job fails to authenticate; build and smoke still run, and workflow_dispatch offers a dry run that stops before publishing. Three things gate a release. The tag must match pyproject.toml. A tag that disagrees with the version it carries is how the published and represented versions come apart. The wheel must contain the policy library. The .rego files come from a submodule and are package data, so a packaging or checkout change can drop them silently. The build job counts them in the archive and checks coverage.json is there, because extraction reads it to find each policy's decision rule. The wheel must produce verdicts. scripts/smoke_test_wheel.py installs it into a clean virtualenv and runs eight real framework evaluations. It deliberately does not run from a source checkout, and refuses to if it detects one: the repository's own .rego files are on disk either way, so a source-tree run passes even when packaging ships nothing. It asserts on counts rather than exit status. `aicertify evaluate` prints "OPA Evaluation: Successful" and exits 0 whether a run produced 29 verdicts or none, so no exit code separates a working release from an empty one. That is the condition v0.8.0 fixed and the reason a release-level check has to exist. What each framework should deliver is read from gopal's coverage.json, filtered to that framework, and not from the packages the evaluation returned. A package that fails to evaluate is absent from the results, so counting only what came back compares a number with itself and always agrees. A floor per framework sits underneath, because delivered == declared holds at zero when the library is missing. Verification: the wheel builds with 198 .rego files and coverage.json, installs into a clean virtualenv, and delivers verdicts for all eight frameworks. The source-checkout guard was confirmed by observing that `import aicertify` from the repository root resolves to the source tree rather than site-packages.
v0.8.0 fixed a condition where an EU AI Act evaluation reported 4 verdicts out of 29 and six other frameworks reported none, while the process exited successfully. Nothing failed because nothing asserted that a framework produces verdicts. These assert it, for all fourteen frameworks that resolve to a policy folder, in three layers. Layer 2, delivered == declared, is the regression itself. What is declared comes from gopal's coverage.json filtered to the framework, never from the packages the evaluation returned. That distinction is the whole test: a package that fails to evaluate is absent from the results and would be absent from an expectation derived from them, so the count agrees with itself and passes. Read against the index instead, the same run reads 5 declared and 4 delivered for education. Layer 1 is a floor, because layer 2 is vacuous alone: with the submodule missing nothing is declared, nothing delivered, and 0 == 0 is green. It is also the only guard against the library shrinking, since a policy removed from gopal outright leaves both counts equal. Floors are 80% of today's declared count, never below 1: one rule rather than fourteen judgements, loose enough that retiring a policy does not fail a release, tight enough that a gutted framework does. Layer 3 covers what the first two cannot see. Both read the index to decide what to expect, so a policy present in the tree but absent from coverage.json is never expected, never delivered and never missed. Layer 3 compares against the packages OPA actually evaluated. education is marked xfail(strict=True) rather than skipped or excluded. Its fairness_and_equity package raises eval_conflict_error in the pinned submodule, so three policies deliver nothing. gopal has the fix. Strict means the exemption cannot outlive the defect: once the submodule is bumped the test passes, pytest reports the unexpected pass as a failure, and the entry has to be removed. Evaluations are cached per framework. Three layers over fourteen frameworks is forty-two tests, and evaluating the library afresh for each would be forty-two full runs to answer fourteen questions. scripts/smoke_test_wheel.py now carries the same floors under the same rule. It stays at eight frameworks: the risk it checks is packaging, and a wheel that drops the policy library fails every framework at once, so eight demonstrates it as well as fourteen and keeps the release gate quick. Verification: 45 passed, 1 xfailed in 97s. The xfail is education's layer 2.
GitHub Actions sets CI=true. OpaEvaluator read that as skip_opa_check and then
set opa_path to None, with the binary installed and on PATH.
None went into argv[0]. Every call through evaluate_policy raised
"sequence item 0: expected str instance, NoneType found" — item 0 being the OPA
executable — and evaluate_by_folder_name reported "No valid results from any
policy evaluation" for every framework.
The other path is worse. _evaluate_with_local_opa checks for None and returns
{"mock_result": true} instead, so an evaluation under CI produced fabricated
verdicts that nothing downstream distinguishes from real ones. That is the same
fail-open shape as issue #78: a run that looks successful and answers nothing.
This is not confined to this repository's CI. The README offers the tool for
"Local or CI", and any consumer pipeline sets CI=true, so policy evaluation was
broken wherever it was most advertised to run.
The flag exists so a missing binary does not abort startup, not so a present one
goes unused. Skipping the check now still resolves the path, through
shutil.which, then OPA_PATH, then a bare "opa" for the call site to fail on if
it genuinely is not there. use_external_server keeps opa_path as None, which is
the one case where no local binary is correct.
Nothing caught this because the tests that exercise OPA resolve the binary
themselves with shutil.which and invoke it directly, so none of them went
through the evaluator's own path. tests/test_framework_golden.py is the first
thing that does, which is how it surfaced.
Verification: opa_path resolves to /usr/local/bin/opa with CI set, was None
before. tests/test_framework_golden.py passes 45 with 1 xfail under CI=true,
matching the local run; before the fix every framework failed. Three regression
tests cover CI set, skip_opa_check set explicitly, and the external-server case;
two of them fail against the previous behaviour.
…overed
gopal#96 corrected industry_specific.education.v1.fairness_and_equity, where two
policies each defined a complete rule named `thresholds` with a different value.
OPA raised eval_conflict_error and failed the whole package, so all three
policies in it delivered no verdict.
Submodule 0936496 to e565a60, which also brings the count guard and
scripts/check-eval-conflicts.sh.
The exemption for education is removed, and it removed itself. The entry was
marked xfail(strict=True) rather than skipped, so closing the gap made the test
pass, and pytest reported the unexpected pass as a failure naming the entry and
the reason to delete it:
FAILED test_every_declared_verdict_is_delivered[education]
[XPASS(strict)] ... Fixed in gopal; remove this entry when the
submodule is bumped past that commit.
KNOWN_DELIVERY_GAPS is now empty and the mechanism is kept for the next one. Its
comment records that this one was exercised rather than only intended.
Verification: 265 passed, 2 skipped, 0 xfailed under CI=true, up from 264 passed
with 1 xfailed before the bump. education now reads 5 declared and 5 delivered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four commits. Closes #60.
Publishing was manual, so it was skipped
GitHub has had v0.8.0 since 28 August while PyPI serves v0.7.3 from 14 May, so
pip install aicertifydoes not install what this repository represents as the product.A tag now publishes, via PyPI Trusted Publishing, with no API token stored here. Three things gate a release:
pyproject.toml. A tag disagreeing with the version it carries is how published and represented versions come apart..regofiles come from a submodule and are package data, so a packaging or checkout change can drop them silently. The build job counts them in the archive and checkscoverage.jsonis present, since extraction reads it to find each policy's decision rule.scripts/smoke_test_wheel.pyinstalls it into a clean virtualenv and runs eight real framework evaluations. It refuses to run from a source checkout: the repository's own.regofiles are on disk either way, so a source-tree run passes even when packaging ships nothing.The smoke test asserts on counts, not exit status.
aicertify evaluateprints "OPA Evaluation: Successful" and exits 0 whether it produced 29 verdicts or none, so no exit code separates a working release from an empty one.Golden tests for every advertised framework
v0.8.0 fixed a condition where EU AI Act reported 4 verdicts out of 29 and six other frameworks reported none, while the process exited successfully. Nothing failed because nothing asserted that a framework produces verdicts. All fourteen frameworks are now covered, in three layers:
declared >= floordelivered == declaredWhat "declared" is measured against decides whether layer 2 works at all. It comes from gopal's
coverage.jsonfiltered to the framework, never from the packages the evaluation returned: a package that fails to evaluate is absent from the results and would be absent from an expectation derived from them, so the count agrees with itself and passes. Read against the index, the same run reads 5 declared and 4 delivered for education.Layer 1 exists because layer 2 is vacuous alone — with the submodule missing, nothing is declared, nothing delivered, and
0 == 0is green. It is also the only guard against the library shrinking, since a policy removed from gopal outright leaves both counts equal. Floors are 80% of today's declared count, never below 1.educationisxfail(strict=True), not skipped. Strict means the exemption cannot outlive the defect: once gopal#96 lands and the submodule is bumped, the test passes, pytest reports the unexpected pass as a failure, and the entry must be removed.OPA was unusable in CI, and one path reported mock verdicts as real
GitHub Actions sets
CI=true.OpaEvaluatorread that asskip_opa_checkand setopa_pathtoNonewith the binary installed and on PATH.Nonewent intoargv[0]. Every call throughevaluate_policyraisedsequence item 0: expected str instance, NoneType found— item 0 being the OPA executable — andevaluate_by_folder_namereported "No valid results from any policy evaluation" for every framework.The other path is worse.
_evaluate_with_local_opachecks forNoneand returns{"mock_result": true}, so an evaluation under CI produced fabricated verdicts that nothing downstream distinguishes from real ones. That is the same fail-open shape as #78.This is not confined to this repository's CI. The README offers the tool for "Local or CI", and any consumer pipeline sets
CI=true, so policy evaluation was broken wherever it is most advertised to run.The flag exists so a missing binary does not abort startup, not so a present one goes unused. Skipping the check now still resolves the path.
use_external_serverkeepsopa_pathasNone, the one case where that is correct.Nothing caught this because every existing test that exercises OPA resolves the binary itself with
shutil.whichand invokes it directly, so none went through the evaluator's own path.tests/test_framework_golden.pyis the first that does.Downloads badge restored (#60)
The badge was removed in #59 pending pepy.tech stats. The issue set the condition for putting it back, and
https://img.shields.io/pepy/dt/aicertify.jsonnow returns{"message":"2.5k","color":"brightgreen"}. Added to the five READMEs the issue names with the alt text it specifies, plusREADME-pypi.md, which is the badge row PyPI itself renders.Verification
CI=true— the exact condition that was failingCI=true, matching local; before the fix every framework failed.regofiles andcoverage.json, installs into a clean virtualenv, and delivers verdicts for all eight smoke frameworksBefore merging
aicertify/opa_policiessubmodule past iteducationentry fromKNOWN_DELIVERY_GAPS— the strict xfail will fail the suite until you do, deliberatelyA PyPI Trusted Publisher is already configured for
release.yaml/ environmentpypi.gh workflow run release.yaml -f dry_run=trueexercises build and smoke without publishing.