diff --git a/.github/workflows/castlabs-vsi-pypi.yml b/.github/workflows/castlabs-vsi-pypi.yml index 45c81f59..724c73df 100644 --- a/.github/workflows/castlabs-vsi-pypi.yml +++ b/.github/workflows/castlabs-vsi-pypi.yml @@ -6,11 +6,11 @@ on: release_tag: description: Existing immutable Castlabs prerelease tag required: true - default: castlabs-v0.37.13.dev1 + default: castlabs-v0.37.13.dev2 type: string concurrency: - group: castlabs-vsi-pypi-0.37.13.dev1 + group: castlabs-vsi-pypi-0.37.13.dev2 cancel-in-progress: false permissions: @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: castlabs/c2pa-python - ref: castlabs-v0.37.13.dev1 + ref: castlabs-v0.37.13.dev2 fetch-depth: 0 path: c2pa-python @@ -45,13 +45,13 @@ jobs: GH_TOKEN: ${{ github.token }} INPUT_RELEASE_TAG: ${{ inputs.release_tag }} run: | - test "${INPUT_RELEASE_TAG}" = castlabs-v0.37.13.dev1 - test "$(python3 c2pa-python/scripts/castlabs_release.py lock-value package.version)" = 0.37.13.dev1 - test "$(gh api repos/castlabs/c2pa-python/releases/tags/castlabs-v0.37.13.dev1 --jq '(.draft == false and .prerelease == true)')" = true + test "${INPUT_RELEASE_TAG}" = castlabs-v0.37.13.dev2 + test "$(python3 c2pa-python/scripts/castlabs_release.py lock-value package.version)" = 0.37.13.dev2 + test "$(gh api repos/castlabs/c2pa-python/releases/tags/castlabs-v0.37.13.dev2 --jq '(.draft == false and .prerelease == true)')" = true SOURCE_SHA=$(git -C c2pa-python rev-parse HEAD) - test "$(git -C c2pa-python rev-list -n 1 castlabs-v0.37.13.dev1)" = "$SOURCE_SHA" + test "$(git -C c2pa-python rev-list -n 1 castlabs-v0.37.13.dev2)" = "$SOURCE_SHA" mkdir -p download dist - gh release download castlabs-v0.37.13.dev1 \ + gh release download castlabs-v0.37.13.dev2 \ --repo castlabs/c2pa-python --dir download python3 c2pa-python/scripts/castlabs_release.py validate-release-assets \ --assets-dir download @@ -63,29 +63,29 @@ jobs: --repo castlabs/c2pa-python \ --signer-workflow castlabs/c2pa-python/.github/workflows/castlabs-vsi-release.yml \ --signer-digest "$SOURCE_SHA" \ - --source-ref refs/tags/castlabs-v0.37.13.dev1 \ + --source-ref refs/tags/castlabs-v0.37.13.dev2 \ --source-digest "$SOURCE_SHA" \ --deny-self-hosted-runners done python3 c2pa-python/scripts/castlabs_release.py validate-evidence \ - --evidence download/c2pa-python-0.37.13.dev1-native-linux-x86_64.tar.gz.evidence.json \ + --evidence download/c2pa-python-0.37.13.dev2-native-linux-x86_64.tar.gz.evidence.json \ --artifact-dir download --source-sha "$SOURCE_SHA" python3 c2pa-python/scripts/castlabs_release.py validate-evidence \ - --evidence download/c2pa-python-0.37.13.dev1-native-windows-x86_64.tar.gz.evidence.json \ + --evidence download/c2pa-python-0.37.13.dev2-native-windows-x86_64.tar.gz.evidence.json \ --artifact-dir download --source-sha "$SOURCE_SHA" python3 c2pa-python/scripts/castlabs_release.py validate-evidence \ - --evidence download/c2pa-python-0.37.13.dev1-py3-none-wheels.tar.gz.evidence.json \ + --evidence download/c2pa-python-0.37.13.dev2-py3-none-wheels.tar.gz.evidence.json \ --artifact-dir download --source-sha "$SOURCE_SHA" python3 c2pa-python/scripts/castlabs_release.py extract \ - --archive download/c2pa-python-0.37.13.dev1-py3-none-wheels.tar.gz \ + --archive download/c2pa-python-0.37.13.dev2-py3-none-wheels.tar.gz \ --destination dist test "$(find dist -maxdepth 1 -name '*.whl' | wc -l)" -eq 2 - cmp dist/c2pa_python-0.37.13.dev1-py3-none-manylinux_2_28_x86_64.whl \ - download/c2pa_python-0.37.13.dev1-py3-none-manylinux_2_28_x86_64.whl - cmp dist/c2pa_python-0.37.13.dev1-py3-none-win_amd64.whl \ - download/c2pa_python-0.37.13.dev1-py3-none-win_amd64.whl + cmp dist/c2pa_python-0.37.13.dev2-py3-none-manylinux_2_28_x86_64.whl \ + download/c2pa_python-0.37.13.dev2-py3-none-manylinux_2_28_x86_64.whl + cmp dist/c2pa_python-0.37.13.dev2-py3-none-win_amd64.whl \ + download/c2pa_python-0.37.13.dev2-py3-none-win_amd64.whl python3 -m pip install packaging==25.0 twine==6.2.0 - python3 -c 'from packaging.version import Version; v=Version("0.37.13.dev1"); assert v.is_prerelease and str(v)=="0.37.13.dev1"' + python3 -c 'from packaging.version import Version; v=Version("0.37.13.dev2"); assert v.is_prerelease and str(v)=="0.37.13.dev2"' python3 -m twine check dist/*.whl - name: Preflight retry-safe PyPI upload set @@ -94,7 +94,7 @@ jobs: run: | STATUS=$(curl --silent --show-error --location \ --output pypi-response.json --write-out '%{http_code}' \ - https://pypi.org/pypi/c2pa-python/0.37.13.dev1/json) + https://pypi.org/pypi/c2pa-python/0.37.13.dev2/json) case "$STATUS" in 200|404) ;; *) echo "unexpected PyPI JSON API status: $STATUS" >&2; exit 1 ;; diff --git a/.github/workflows/castlabs-vsi-release.yml b/.github/workflows/castlabs-vsi-release.yml index 7a2165fe..772a7e50 100644 --- a/.github/workflows/castlabs-vsi-release.yml +++ b/.github/workflows/castlabs-vsi-release.yml @@ -3,11 +3,11 @@ name: Castlabs VSI prerelease on: push: tags: - - castlabs-v0.37.13.dev1 + - castlabs-v0.37.13.dev2 workflow_dispatch: inputs: source_sha: - description: Full Python SHA (feat/trusted-vsi-functional tip for 0.37.13.dev1; paired source for trusted_vsi_only) + description: Full Python SHA (feat/trusted-vsi-functional tip for 0.37.13.dev2; paired source for trusted_vsi_only) required: true type: string trusted_vsi_only: @@ -17,7 +17,7 @@ on: default: false concurrency: - group: castlabs-vsi-release-0.37.13.dev1 + group: castlabs-vsi-release-0.37.13.dev2 cancel-in-progress: false permissions: @@ -72,7 +72,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: castlabs/c2pa-rs - ref: 3328b2b440fd07d9f857cbefa3c6d565093425de + ref: afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95 fetch-depth: 1 path: c2pa-rs @@ -123,7 +123,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: castlabs/c2pa-rs - ref: 3328b2b440fd07d9f857cbefa3c6d565093425de + ref: afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95 fetch-depth: 1 path: c2pa-rs @@ -195,7 +195,7 @@ jobs: test "$(find wheelhouse -maxdepth 1 -name "*.whl" | wc -l)" -eq 1 WHEEL=$(find wheelhouse -maxdepth 1 -name "*.whl") case "$(basename "$WHEEL")" in - c2pa_python-0.37.13.dev1-py3-none-manylinux_2_28_x86_64.whl) ;; + c2pa_python-0.37.13.dev2-py3-none-manylinux_2_28_x86_64.whl) ;; *) echo "unexpected Linux wheel: $WHEEL" >&2; exit 1 ;; esac /opt/python/cp310-cp310/bin/python -m auditwheel show "$WHEEL" @@ -215,7 +215,7 @@ jobs: SOURCE_DATE_EPOCH: ${{ needs.prepare.outputs.source_epoch }} run: | cd c2pa-python - BUNDLE=release-out/c2pa-python-0.37.13.dev1-native-linux-x86_64.tar.gz + BUNDLE=release-out/c2pa-python-0.37.13.dev2-native-linux-x86_64.tar.gz python3 scripts/castlabs_release.py pack --output "$BUNDLE" \ --epoch "$SOURCE_DATE_EPOCH" \ --member x86_64-unknown-linux-gnu/libc2pa_c.so=release-out/libc2pa_c.so @@ -278,7 +278,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: castlabs/c2pa-rs - ref: 3328b2b440fd07d9f857cbefa3c6d565093425de + ref: afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95 fetch-depth: 1 path: c2pa-rs @@ -351,7 +351,7 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $wheels = @(Get-ChildItem dist/*.whl) if ($wheels.Count -ne 1) { throw "expected exactly one Windows wheel" } - if ($wheels[0].Name -ne "c2pa_python-0.37.13.dev1-py3-none-win_amd64.whl") { + if ($wheels[0].Name -ne "c2pa_python-0.37.13.dev2-py3-none-win_amd64.whl") { throw "unexpected Windows wheel $($wheels[0].Name)" } python -m twine check $wheels[0].FullName @@ -371,7 +371,7 @@ jobs: $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $true Push-Location c2pa-python - $bundle = "release-out/c2pa-python-0.37.13.dev1-native-windows-x86_64.tar.gz" + $bundle = "release-out/c2pa-python-0.37.13.dev2-native-windows-x86_64.tar.gz" python scripts/castlabs_release.py pack --output $bundle ` --epoch $env:SOURCE_DATE_EPOCH ` --member x86_64-pc-windows-msvc/c2pa_c.dll=release-out/c2pa_c.dll @@ -424,7 +424,7 @@ jobs: - name: Test installed wheel in a clean environment shell: bash env: - CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev1 + CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev2 CASTLABS_RELEASE_SMOKE_REQUIRED: "1" C2PA_SOURCE_BUILD_VERSION: 0.92.0-dev run: | @@ -432,6 +432,8 @@ jobs: python -m venv "${RUNNER_TEMP}/wheel-test" "${RUNNER_TEMP}/wheel-test/bin/python" -m pip install --upgrade pip "${RUNNER_TEMP}/wheel-test/bin/python" -m pip install release-out/*.whl + # pytest is no longer a runtime dependency of the wheel (upstream 4d909cf). + "${RUNNER_TEMP}/wheel-test/bin/python" -m pip install pytest==8.4.1 "${RUNNER_TEMP}/wheel-test/bin/python" -m pytest -q \ c2pa-python/tests/test_castlabs_release_smoke.py if [[ "${{ matrix.python }}" == "3.10" ]]; then @@ -464,7 +466,7 @@ jobs: - name: Test installed wheel in a clean environment shell: pwsh env: - CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev1 + CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev2 CASTLABS_RELEASE_SMOKE_REQUIRED: "1" C2PA_SOURCE_BUILD_VERSION: 0.92.0-dev run: | @@ -476,6 +478,7 @@ jobs: & "$env:RUNNER_TEMP\wheel-test\Scripts\python.exe" -m pip install --upgrade pip $wheel = (Get-ChildItem release-out/*.whl).FullName & "$env:RUNNER_TEMP\wheel-test\Scripts\python.exe" -m pip install $wheel + & "$env:RUNNER_TEMP\wheel-test\Scripts\python.exe" -m pip install pytest==8.4.1 & "$env:RUNNER_TEMP\wheel-test\Scripts\python.exe" -m pytest -q ` c2pa-python/tests/test_castlabs_release_smoke.py if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -487,7 +490,7 @@ jobs: release: needs: [prepare, linux, windows, test-linux-wheel, test-windows-wheel] - if: github.ref == 'refs/tags/castlabs-v0.37.13.dev1' + if: github.ref == 'refs/tags/castlabs-v0.37.13.dev2' runs-on: ubuntu-24.04 timeout-minutes: 30 environment: castlabs-vsi-release @@ -522,7 +525,7 @@ jobs: LINUX_WHEEL=$(find collected/linux -maxdepth 1 -name "*.whl") WINDOWS_WHEEL=$(find collected/windows -maxdepth 1 -name "*.whl") test "$(find collected/linux collected/windows -name "*.whl" | wc -l)" -eq 2 - BUNDLE=assets/c2pa-python-0.37.13.dev1-py3-none-wheels.tar.gz + BUNDLE=assets/c2pa-python-0.37.13.dev2-py3-none-wheels.tar.gz python3 c2pa-python/scripts/castlabs_release.py pack \ --output "$BUNDLE" --epoch "$SOURCE_DATE_EPOCH" \ --member "$(basename "$LINUX_WHEEL")=$LINUX_WHEEL" \ @@ -539,13 +542,13 @@ jobs: python3 c2pa-python/scripts/castlabs_release.py checksum \ --artifact "$BUNDLE.evidence.json" cp "$LINUX_WHEEL" "$WINDOWS_WHEEL" assets/ - cp collected/linux/c2pa-python-0.37.13.dev1-native-linux-x86_64.tar.gz* assets/ - cp collected/windows/c2pa-python-0.37.13.dev1-native-windows-x86_64.tar.gz* assets/ + cp collected/linux/c2pa-python-0.37.13.dev2-native-linux-x86_64.tar.gz* assets/ + cp collected/windows/c2pa-python-0.37.13.dev2-native-windows-x86_64.tar.gz* assets/ python3 c2pa-python/scripts/castlabs_release.py validate-evidence \ - --evidence assets/c2pa-python-0.37.13.dev1-native-linux-x86_64.tar.gz.evidence.json \ + --evidence assets/c2pa-python-0.37.13.dev2-native-linux-x86_64.tar.gz.evidence.json \ --artifact-dir assets --source-sha "${{ needs.prepare.outputs.source_sha }}" python3 c2pa-python/scripts/castlabs_release.py validate-evidence \ - --evidence assets/c2pa-python-0.37.13.dev1-native-windows-x86_64.tar.gz.evidence.json \ + --evidence assets/c2pa-python-0.37.13.dev2-native-windows-x86_64.tar.gz.evidence.json \ --artifact-dir assets --source-sha "${{ needs.prepare.outputs.source_sha }}" python3 c2pa-python/scripts/castlabs_release.py validate-evidence \ --evidence "$BUNDLE.evidence.json" --artifact-dir assets \ @@ -559,7 +562,7 @@ jobs: - name: Require immutable release tag shell: bash run: | - TAG=castlabs-v0.37.13.dev1 + TAG=castlabs-v0.37.13.dev2 git -C c2pa-python fetch origin "refs/tags/${TAG}:refs/tags/${TAG}" test "$(git -C c2pa-python rev-list -n 1 "$TAG")" = \ "${{ needs.prepare.outputs.source_sha }}" @@ -575,7 +578,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - TAG=castlabs-v0.37.13.dev1 + TAG=castlabs-v0.37.13.dev2 SOURCE_SHA="${{ needs.prepare.outputs.source_sha }}" RELEASE_NAME=$(python3 c2pa-python/scripts/castlabs_release.py release-value name) RELEASE_BODY=$(python3 c2pa-python/scripts/castlabs_release.py release-value body) @@ -601,7 +604,7 @@ jobs: } discover_release() { gh api --paginate repos/castlabs/c2pa-python/releases \ - --jq '.[] | select(.tag_name == "castlabs-v0.37.13.dev1")' > "$1" + --jq '.[] | select(.tag_name == "castlabs-v0.37.13.dev2")' > "$1" return 0 } query_release_id() { diff --git a/.github/workflows/trusted-vsi-paired.yml b/.github/workflows/trusted-vsi-paired.yml index fad74d99..7983e116 100644 --- a/.github/workflows/trusted-vsi-paired.yml +++ b/.github/workflows/trusted-vsi-paired.yml @@ -35,9 +35,9 @@ jobs: C2PA_REQUIRE_SIGN_LADDER: "1" C2PA_REQUIRE_FRAGMENTED_FILES: "1" C2PA_FRAGMENT_OFFSET_REQUIRED: "1" - FUNCTIONAL_BUILD_VERSION: 0.37.13.dev1 + FUNCTIONAL_BUILD_VERSION: 0.37.13.dev2 # The release smoke asserts the installed distribution version. - CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev1 + CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev2 steps: - name: Require a full Python source SHA shell: bash @@ -52,9 +52,9 @@ jobs: with: repository: castlabs/c2pa-rs # Revision-3 trusted VSI plus offset-aware fragment verification: - # the same native commit the 0.37.13.dev1 release lock pins. This job + # the same native commit the 0.37.13.dev2 release lock pins. This job # is qualification-only and never produces release artifacts. - ref: 3328b2b440fd07d9f857cbefa3c6d565093425de + ref: afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95 path: paired-rust - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: diff --git a/README.md b/README.md index 537eaab6..cfbda439 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH The [trusted-processor API](docs/usage.md#trusted-processor-vsi-unreleased-functional-api) is separate from complete-buffer VSI and immutable dev5. It is first released in -`0.37.13.dev1`. Its probes are true only on a native library reporting trusted +`0.37.13.dev2`. Its probes are true only on a native library reporting trusted contract revision 3 and capability mask 63 (the pinned release native does); otherwise every probe is false and session construction/operations fail before side effects. Expert signing accepts only exact `Sig_structure` bytes and returns @@ -118,8 +118,8 @@ library (`-k "not paired"`). Separately, both it and the dedicated workflow use Linux/Windows source builds and the full focused `tests/test_trusted_vsi_api.py` with `C2PA_TRUSTED_VSI_ABI_REQUIRED=1`. Missing reduced ABI symbols fail rather than skip. The paired Rust source is pinned to -`castlabs/c2pa-rs@3328b2b440fd07d9f857cbefa3c6d565093425de`, the same commit as -the 0.37.13.dev1 release lock; the job logs resolved Python/Rust SHAs. The +`castlabs/c2pa-rs@afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95`, the same commit as +the 0.37.13.dev2 release lock; the job logs resolved Python/Rust SHAs. The paired job is qualification only and is not release-artifact evidence. For the dedicated workflow, manually select the trusted-API workflow revision, @@ -147,7 +147,7 @@ imports (`python setup.py egg_info`, without a native download or wheel build). ### Immutable Castlabs VSI prerelease process -The dedicated [`Castlabs VSI prerelease`](.github/workflows/castlabs-vsi-release.yml) workflow builds version `0.37.13.dev1` for Linux x86-64 and Windows x86-64. It does not call `scripts/download_artifacts.py`: both native libraries are compiled with Cargo `--locked` and `CARGO_BUILD_JOBS=1` from the exact [Castlabs c2pa-rs](https://github.com/castlabs/c2pa-rs) commit in [`release/castlabs-vsi-inputs.lock.json`](release/castlabs-vsi-inputs.lock.json). The lock also fixes the Rust 1.96.0 toolchain coordinated with c2pa-rs qualification, checksum-verified rustup installers, no-default-feature set, target set, and the Linux manylinux container digest. The legacy `build.yml` explicitly excludes `castlabs-v*` tag pushes and guards its jobs and PyPI publisher against manual dispatch on those tags; the dedicated workflow alone owns them. Ordinary non-Castlabs tag releases retain the legacy behavior, while manual legacy publication additionally requires `refs/heads/main` and a final `X.Y.Z` package version. +The dedicated [`Castlabs VSI prerelease`](.github/workflows/castlabs-vsi-release.yml) workflow builds version `0.37.13.dev2` for Linux x86-64 and Windows x86-64. It does not call `scripts/download_artifacts.py`: both native libraries are compiled with Cargo `--locked` and `CARGO_BUILD_JOBS=1` from the exact [Castlabs c2pa-rs](https://github.com/castlabs/c2pa-rs) commit in [`release/castlabs-vsi-inputs.lock.json`](release/castlabs-vsi-inputs.lock.json). The lock also fixes the Rust 1.96.0 toolchain coordinated with c2pa-rs qualification, checksum-verified rustup installers, no-default-feature set, target set, and the Linux manylinux container digest. The legacy `build.yml` explicitly excludes `castlabs-v*` tag pushes and guards its jobs and PyPI publisher against manual dispatch on those tags; the dedicated workflow alone owns them. Ordinary non-Castlabs tag releases retain the legacy behavior, while manual legacy publication additionally requires `refs/heads/main` and a final `X.Y.Z` package version. The immutable `castlabs-v0.37.8.dev1` tag records failed prerelease workflow run `34030865864`. Linux compiled successfully but its combined DynamicAssertion-plus-VSI smoke used a 5-byte callback result for a 64-byte reservation and later failed with `assertion.bmffHash.mismatch`; Windows compiled successfully and failed only in platform-sensitive wheel metadata parsing. The Linux failure was an undersized callback contract violation, not an inherent incompatibility between DynamicAssertions and VSI. No dev1 draft or GitHub release was created, and the tag remains unchanged. @@ -157,9 +157,11 @@ The immutable `castlabs-v0.37.8.dev3` tag records failed prerelease workflow run The published `castlabs-v0.37.8.dev4` prerelease and its tag are immutable historical release artifacts and remain unchanged. Dev4 used the earlier maximum-size DynamicAssertion callback contract, which accepted outputs shorter than `reserve_size`. Dev5 intentionally makes that contract stricter: exact callback sizing is enforced for every signing path, and both undersized and oversized outputs are rejected. +The immutable `castlabs-v0.37.13.dev1` tag (source `78771fa`, native `3328b2b4`) records failed prerelease workflow run `38786913569`. The Linux build succeeded, but every Linux installed-wheel test job (Python 3.10 through 3.13) failed with `No module named pytest`: pytest is no longer a runtime dependency of the wheel (upstream `4d909cf`) and the clean test venv did not install it. The Windows jobs and the release job were cancelled. No dev1 draft or GitHub release was created, and the tag remains unchanged. Dev2 installs `pytest==8.4.1` after the wheel in both the Linux and Windows installed-wheel steps, and pairs with native `afd6fffe`, which merges the Conformance Program 0.2 / C2PA 2.4 line into the 1.0.8 trusted VSI native line. + Release controls: -- Configure `castlabs-v0.37.13.dev1` as a protected tag and require approval for the `castlabs-vsi-release` environment. +- Configure `castlabs-v0.37.13.dev2` as a protected tag and require approval for the `castlabs-vsi-release` environment. - A tag run accepts only that exact tag. A manual run accepts only a full 40-character SHA equal to the tip of `feat/trusted-vsi-functional` and is build/test-only; draft release creation and provenance signing require the exact protected tag-triggered run. - Source checkout URLs, full SHAs, package/native versions, and the pinned `Cargo.lock` SHA-256 are asserted before each build. - The Linux wheel is built and repaired in the digest-pinned `manylinux_2_28_x86_64` image. Every wheel is tested without capability skips on clean Python 3.10 through 3.13; Python 3.10 also runs the broader unit and fragmented-file suites. @@ -167,9 +169,9 @@ Release controls: - [`Promote Castlabs VSI prerelease to PyPI`](.github/workflows/castlabs-vsi-pypi.yml) is a separate manual workflow protected by the `pypipublish` environment. It requires the finalized GitHub prerelease and verifies every wheel, bundle, evidence document, feature report, and checksum against provenance from the exact dedicated signer workflow, tag, and source SHA. Checksums and strict evidence then validate the final-wheel metadata and embedded native digests before promotion. - PyPI does not provide a multi-file transaction. Promotion therefore queries the version JSON API first. A fresh release uploads both platform wheels together; a retry accepts an existing wheel only when PyPI's SHA-256 exactly matches the local attested wheel, then uploads all missing wheels together. A mismatched or unexpected PyPI file fails. This policy intentionally publishes no source distribution for the two-platform prerelease and safely recovers from an unavoidable prior partial wheel upload. -Intermediate Actions artifacts expire after seven days. A draft resume always rebuilds both platforms and requires byte-identical output for every already-uploaded asset; rerunning only an expired release job cannot recover its inputs, so rerun the complete tag workflow. If reproducibility or an operator mistake leaves a mismatched prior draft, the workflow preserves it and reports failure. Inspect it manually, then remove only that draft with `gh release delete castlabs-v0.37.13.dev1 --repo castlabs/c2pa-python --yes` before rerunning the complete protected-tag workflow. Never use clobber to repair a mismatch. +Intermediate Actions artifacts expire after seven days. A draft resume always rebuilds both platforms and requires byte-identical output for every already-uploaded asset; rerunning only an expired release job cannot recover its inputs, so rerun the complete tag workflow. If reproducibility or an operator mistake leaves a mismatched prior draft, the workflow preserves it and reports failure. Inspect it manually, then remove only that draft with `gh release delete castlabs-v0.37.13.dev2 --repo castlabs/c2pa-python --yes` before rerunning the complete protected-tag workflow. Never use clobber to repair a mismatch. -`tests/test_castlabs_release_smoke.py` is intentionally gated for ordinary contributors because upstream native binaries do not provide every Castlabs capability. Bare pytest skips that module unless `CASTLABS_RELEASE_SMOKE_REQUIRED=1` is set. The dedicated wheel-test jobs always set it; with the gate enabled, package version, dynamic assertions, fragmented files, offset-aware fragment verification, VSI callback signing, recovery, explicit time, and MFHD probing are hard assertions with no capability skip. Contributors with a qualified installed wheel can run the same gate explicitly with `CASTLABS_RELEASE_SMOKE_REQUIRED=1 python -m pytest -q tests/test_castlabs_release_smoke.py`. `CASTLABS_RELEASE_EXPECTED_VERSION` exists only for testing an already-built prior candidate during release-pipeline repair; the dedicated tagged workflow explicitly fixes it to `0.37.13.dev1`. +`tests/test_castlabs_release_smoke.py` is intentionally gated for ordinary contributors because upstream native binaries do not provide every Castlabs capability. Bare pytest skips that module unless `CASTLABS_RELEASE_SMOKE_REQUIRED=1` is set. The dedicated wheel-test jobs always set it; with the gate enabled, package version, dynamic assertions, fragmented files, offset-aware fragment verification, VSI callback signing, recovery, explicit time, and MFHD probing are hard assertions with no capability skip. Contributors with a qualified installed wheel can run the same gate explicitly with `CASTLABS_RELEASE_SMOKE_REQUIRED=1 python -m pytest -q tests/test_castlabs_release_smoke.py`. `CASTLABS_RELEASE_EXPECTED_VERSION` exists only for testing an already-built prior candidate during release-pipeline repair; the dedicated tagged workflow explicitly fixes it to `0.37.13.dev2`. At the pinned c2pa-rs commit, VSI initialization signs through `Builder::from_shared_context`, so it inherits DynamicAssertions registered on the Context's claim signer. The common Python callback bridge enforces `reserve_size` as the exact serialized CBOR size for every signing path. The earlier `assertion.bmffHash.mismatch` came from returning only 5 bytes for a 64-byte reservation; it was not an inherent DynamicAssertion-plus-VSI incompatibility. The release smoke now returns valid exact-size CBOR, requires clean Reader validation for both standalone image and combined VSI init signing, and exercises combined media signing and recovery without an xfail. diff --git a/docs/release-notes.md b/docs/release-notes.md index c8302e20..71162078 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,11 +1,46 @@ # Release notes -## Version 0.37.13.dev1 +## Version 0.37.13.dev2 -Castlabs VSI prerelease (tag `castlabs-v0.37.13.dev1`, released from -`feat/trusted-vsi-functional`). It is the first release of the trusted-processor -VSI line. The immutable `0.37.8.dev5` release inputs and artifacts -(`feat/live-video-vsi`) stay unchanged. +Castlabs VSI prerelease (tag `castlabs-v0.37.13.dev2`, released from +`feat/trusted-vsi-functional`). It is the first published release of the +trusted-processor VSI line and true-merges the Conformance Program 0.2 / +C2PA 2.4 line (`feat/conformance-validation-v02`, published as +`0.37.12.dev3`/`0.37.12.dev4`) into it. The immutable `0.37.8.dev5` and +`0.37.12.dev*` release inputs and artifacts stay unchanged. + +- Native `castlabs/c2pa-rs` `afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95` + (workspace `0.92.0-dev`, `Cargo.lock` SHA-256 + `f7022b3e05c2dc2080946064b386b293b29c3a60ca9daf344e1b4e3fc398c370`, + unchanged from dev1), built with Rust `1.96.0`. It is the 1.0.8 trusted VSI + native line (`3328b2b4`) merged with the native conformance line. +- `Reader.crjson()` emits crJSON 2.4: `validationResults.specVersion` reports + `2.4.0`, the post-2.4 `isUpdateManifest` / `isCompressedManifest` manifest + fields are not emitted, and `specVersion` is placed per 2.4 in + `claim_generator_info`. +- `Reader.crjson()` is checked: the native export returns NULL with an error + instead of `"{}"`, so exporter failures raise `C2paError`. +- `verify.validation_time` (RFC 3339) selects the evaluation instant for + certificate, chain and OCSP decisions and the reported `validationTime`. + Malformed values are rejected when the context is built. +- Purpose-scoped trust anchors (`trust.anchors` with `trust_kind` + `manifest`/`tsa`/`cawg`) are always strict on this line: an anchor + authorizes only its declared purpose. The conformance line's + `verify.strict_trust_purposes` setting (and its non-strict fallback) does + not exist here: the key is accepted and silently ignored, so anchors must + carry the correct `trust_kind`. +- The installed-wheel release tests install `pytest==8.4.1` after the wheel + on Linux and Windows; pytest is no longer a runtime dependency of the wheel + (upstream `4d909cf`). + +### 0.37.13.dev1 (not released) + +Tag `castlabs-v0.37.13.dev1` (source `78771fa`, native `3328b2b4`) ran +workflow `38786913569`: the Linux build succeeded but every Linux +installed-wheel test job failed with `No module named pytest` because the +clean test venv did not install pytest; the Windows and release jobs were +cancelled. No dev1 draft or GitHub release was created and the tag remains +unchanged. Everything below ships first in dev2. - Native `castlabs/c2pa-rs` `3328b2b440fd07d9f857cbefa3c6d565093425de` (workspace `0.92.0-dev`, `Cargo.lock` SHA-256 @@ -71,6 +106,41 @@ VSI line. The immutable `0.37.8.dev5` release inputs and artifacts retained, and registry tags are recognized only as the error prefix (a tag quoted inside another error's payload no longer establishes ownership). +## Version 0.37.12.dev4 + +Supersedes the published 0.37.12.dev3 (which remains unchanged). Native +`castlabs/c2pa-rs` `e1cc897d5c04d11e7773f54a659f98ec89ae224f` makes `Reader.crjson()` +conform to the published crJSON 2.4 schema: `validationResults.specVersion` +reports `2.4.0` (the validator's C2PA version) instead of `2.3.0`, and the +post-2.4 `isUpdateManifest` / `isCompressedManifest` manifest fields are no +longer emitted. + +## Version 0.37.12.dev3 + +Supersedes 0.37.12.dev2 (run `35997070721`, cancelled before publication): the +native source is re-pinned to `6fa3e01097a87ee9265c97e00d9ec12be3052288`, whose +only change over dev2's `6ca7fb16` aligns c2patool CLI tests with contentauth +main, so native qualification passes on both platforms. + +### 0.37.12.dev2 (not released) + +Castlabs Conformance Program 0.2 / C2PA 2.4 candidate prerelease. Successor to +0.37.12.dev1, whose tag-triggered run `35961050991` built both platforms but +failed installed-wheel tests (pytest no longer a runtime dependency) and whose +native source failed one upstream test under `http_reqwest_blocking`; no dev1 +draft or GitHub release was created. + +- Native `castlabs/c2pa-rs` `6ca7fb168b10bde7a1ef8bb8e577ffbae73233c7` (dev3: `6fa3e010`): the + live-video VSI line merged with contentauth main `6c92bc32` (C2PA 2.4 + validation), plus the single-file fMP4 Merkle and TFRA fixes. +- `verify.validation_time` (RFC 3339) selects the evaluation instant for + certificate, chain and OCSP decisions and the reported `validationTime`. +- Purpose-scoped trust anchors (`trust.anchors` with `trust_kind` + `manifest`/`tsa`/`cawg`) and `verify.strict_trust_purposes`. +- `Reader.crjson()` is checked: exporter failures raise instead of `{}`. +- 2.4 `specVersion` in `claim_generator_info` serializes correctly, and the + default EKU allow-list survives context trust settings. + ## Version 0.37.8.dev5 ### Breaking changes diff --git a/docs/trusted-vsi-python-contract.md b/docs/trusted-vsi-python-contract.md index cd3c4f51..600df64f 100644 --- a/docs/trusted-vsi-python-contract.md +++ b/docs/trusted-vsi-python-contract.md @@ -3,11 +3,12 @@ Status: integrated, unreleased PR4 review changes requiring native trusted-VSI contract revision 3 (state version 3 and typed expert VSI payloads), following `c2pa-rs` -`docs/trusted-vsi-native-contract.md`. The Python identity is `0.37.13.dev1`, +`docs/trusted-vsi-native-contract.md`. The Python identity is `0.37.13.dev2`, prepared as the Castlabs VSI prerelease on native -`3328b2b440fd07d9f857cbefa3c6d565093425de`; immutable dev5 release inputs and +`afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95`; immutable dev5 release inputs and artifacts are unchanged. The paired source workflow pins the same native commit -(previously `a6d4cdcc05638ee8dd0afce7fa5c850d7031a80c`) for revision-3 +(previously `3328b2b440fd07d9f857cbefa3c6d565093425de` for the unpublished +0.37.13.dev1 and `a6d4cdcc05638ee8dd0afce7fa5c850d7031a80c` before it) for revision-3 qualification. Select a reviewed local library with `C2PA_LIBRARY_NAME` for integration tests. Version `0.92.0-dev` and mask 63 do not establish contract identity; Python now requires the explicit native contract-revision probe. @@ -243,14 +244,14 @@ CBOR, BMFF or validation results. ` first requires all probes true against that library, then builds a NON-PUBLISHING wheel and sdist in a temporary staging copy with `FUNCTIONAL_BUILD_VERSION` (default: the checkout's source identity -`0.37.13.dev1`; rejects `0.37.8.*`, dev5, release versions and anything older -than the source identity, including `0.37.13.dev0` and the historical +`0.37.13.dev2`; rejects `0.37.8.*`, dev5, release versions and anything older +than the source identity, including `0.37.13.dev1`, `0.37.13.dev0` and the historical `0.37.9.dev0`). `C2PA_SOURCE_BUILD_VERSION=0.92.0-dev` identifies the native library only. The release lock and tooling now match this source -(`castlabs-v0.37.13.dev1`); the immutable dev5 release remains historical. +(`castlabs-v0.37.13.dev2`); the immutable dev5 release remains historical. `scripts/qualify_trusted_vsi_functional.py --wheel --venv ---version 0.37.13.dev1` installs the wheel into an isolated venv, strips +--version 0.37.13.dev2` installs the wheel into an isolated venv, strips `PYTHONPATH`/`C2PA_LIBRARY_NAME`, and runs `test_trusted_vsi_api.py`, `test_fragmented_files.py`, `test_sign_ladder.py`, `test_native_ownership.py` and `test_native_ownership_opaque.py` with `C2PA_TRUSTED_VSI_ABI_REQUIRED=1`, @@ -263,8 +264,8 @@ Paired tests require the full native library and FAIL under `C2PA_TRUSTED_VSI_ABI_REQUIRED=1` (all Linux/Windows qualification jobs); they skip only in ad-hoc local runs. `.github/workflows/trusted-vsi-paired.yml` builds the native with Rust 1.96.0 from castlabs/c2pa-rs -`3328b2b440fd07d9f857cbefa3c6d565093425de`, the same commit as the -`castlabs-v0.37.13.dev1` release lock (trusted contract revision 3, mask 63, +`afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95`, the same commit as the +`castlabs-v0.37.13.dev2` release lock (trusted contract revision 3, mask 63, offset-aware fragment exports). It sets `C2PA_FRAGMENT_OFFSET_REQUIRED=1`, so a native library without the fragment-offset probe fails rather than skips. Older pins (`6b506352`, `203dc08d`, `5c186c07`) emitted state version 2 and lacked the diff --git a/pyproject.toml b/pyproject.toml index 3c49910a..827f4385 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "c2pa-python" -version = "0.37.13.dev1" +version = "0.37.13.dev2" requires-python = ">=3.10" description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library" readme = { file = "README.md", content-type = "text/markdown" } diff --git a/release/castlabs-release-evidence.schema.json b/release/castlabs-release-evidence.schema.json index 6a3d73b8..18d07134 100644 --- a/release/castlabs-release-evidence.schema.json +++ b/release/castlabs-release-evidence.schema.json @@ -13,7 +13,7 @@ "properties": { "type": {"enum": ["native-bundle", "wheel-bundle"]}, "package": {"const": "c2pa-python"}, - "version": {"const": "0.37.13.dev1"}, + "version": {"const": "0.37.13.dev2"}, "file": {"type": "string", "minLength": 1}, "size": {"type": "integer", "minimum": 1}, "sha256": {"$ref": "#/$defs/sha256"}, @@ -49,7 +49,7 @@ "properties": { "repository": {"const": "castlabs/c2pa-rs"}, "url": {"const": "https://github.com/castlabs/c2pa-rs.git"}, - "commit": {"const": "3328b2b440fd07d9f857cbefa3c6d565093425de"}, + "commit": {"const": "afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95"}, "version": {"const": "0.92.0-dev"}, "cargoLockSha256": {"const": "f7022b3e05c2dc2080946064b386b293b29c3a60ca9daf344e1b4e3fc398c370"} } diff --git a/release/castlabs-vsi-inputs.lock.json b/release/castlabs-vsi-inputs.lock.json index cfa8d565..5e8a982c 100644 --- a/release/castlabs-vsi-inputs.lock.json +++ b/release/castlabs-vsi-inputs.lock.json @@ -2,18 +2,18 @@ "schemaVersion": 1, "package": { "name": "c2pa-python", - "version": "0.37.13.dev1" + "version": "0.37.13.dev2" }, "pythonSource": { "repository": "castlabs/c2pa-python", "url": "https://github.com/castlabs/c2pa-python.git", "releaseBranch": "feat/trusted-vsi-functional", - "releaseTag": "castlabs-v0.37.13.dev1" + "releaseTag": "castlabs-v0.37.13.dev2" }, "rustSource": { "repository": "castlabs/c2pa-rs", "url": "https://github.com/castlabs/c2pa-rs.git", - "commit": "3328b2b440fd07d9f857cbefa3c6d565093425de", + "commit": "afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95", "version": "0.92.0-dev", "cargoLockSha256": "f7022b3e05c2dc2080946064b386b293b29c3a60ca9daf344e1b4e3fc398c370" }, diff --git a/release/castlabs-vsi-inputs.schema.json b/release/castlabs-vsi-inputs.schema.json index e0460536..6e0a84bd 100644 --- a/release/castlabs-vsi-inputs.schema.json +++ b/release/castlabs-vsi-inputs.schema.json @@ -30,7 +30,7 @@ "package": { "type": "object", "additionalProperties": false, "required": ["name", "version"], - "properties": {"name": {"const": "c2pa-python"}, "version": {"const": "0.37.13.dev1"}} + "properties": {"name": {"const": "c2pa-python"}, "version": {"const": "0.37.13.dev2"}} }, "pythonSource": { "type": "object", "additionalProperties": false, @@ -39,7 +39,7 @@ "repository": {"const": "castlabs/c2pa-python"}, "url": {"const": "https://github.com/castlabs/c2pa-python.git"}, "releaseBranch": {"const": "feat/trusted-vsi-functional"}, - "releaseTag": {"const": "castlabs-v0.37.13.dev1"} + "releaseTag": {"const": "castlabs-v0.37.13.dev2"} } }, "rustSource": { @@ -48,7 +48,7 @@ "properties": { "repository": {"const": "castlabs/c2pa-rs"}, "url": {"const": "https://github.com/castlabs/c2pa-rs.git"}, - "commit": {"const": "3328b2b440fd07d9f857cbefa3c6d565093425de"}, + "commit": {"const": "afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95"}, "version": {"const": "0.92.0-dev"}, "cargoLockSha256": {"const": "f7022b3e05c2dc2080946064b386b293b29c3a60ca9daf344e1b4e3fc398c370"} } diff --git a/scripts/castlabs_release.py b/scripts/castlabs_release.py index a093f741..35995525 100644 --- a/scripts/castlabs_release.py +++ b/scripts/castlabs_release.py @@ -26,24 +26,26 @@ LOCK_PATH = ROOT / "release" / "castlabs-vsi-inputs.lock.json" SHA256_RE = re.compile(r"^[0-9a-f]{64}$") SHA_RE = re.compile(r"^[0-9a-f]{40}$") -RELEASE_VERSION = "0.37.13.dev1" -RUST_COMMIT = "3328b2b440fd07d9f857cbefa3c6d565093425de" +RELEASE_VERSION = "0.37.13.dev2" +RUST_COMMIT = "afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95" CARGO_LOCK_SHA256 = "f7022b3e05c2dc2080946064b386b293b29c3a60ca9daf344e1b4e3fc398c370" RUST_TOOLCHAIN = "1.96.0" MANYLINUX_IMAGE = "quay.io/pypa/manylinux_2_28_x86_64" MANYLINUX_DIGEST = ( "sha256:0d9c2a66a745961947a8cecbe217ca0a7ee7a5849ba2517f20f9581d18444977" ) -RELEASE_TAG = "castlabs-v0.37.13.dev1" +RELEASE_TAG = "castlabs-v0.37.13.dev2" RELEASE_NAME = f"Castlabs c2pa-python {RELEASE_VERSION} (VSI)" RELEASE_BODY_MARKER = f"castlabs-vsi-release:{RELEASE_VERSION}" RELEASE_IDENTITY_TEXT = f"Castlabs c2pa-python {RELEASE_VERSION} immutable prerelease" RELEASE_BODY = ( f"{RELEASE_BODY_MARKER}\n\n" f"{RELEASE_IDENTITY_TEXT}. Trusted-processor VSI line (native contract " - "revision 3, capability mask 63) on castlabs/c2pa-rs 3328b2b4 (0.92.0-dev, " - "Rust 1.96.0): offset-aware single-file fMP4 fragment verification, default " - "EKU trust-policy fix and patched event-listener/rustls/quinn-proto. See " + "revision 3, capability mask 63) on castlabs/c2pa-rs afd6fffe (0.92.0-dev, " + "Rust 1.96.0), merged with the Conformance Program 0.2 / C2PA 2.4 line: " + "crJSON 2.4 output with checked export, caller-controlled validation time, " + "offset-aware single-file fMP4 fragment verification and the default EKU " + "trust-policy fix. Successor to dev1 run 38786913569; no dev1 release. See " "the attached schema-2 evidence and SHA-256 sidecars." ) @@ -220,12 +222,12 @@ def validate_lock(lock: dict[str, Any]) -> None: if set(lock) != required or lock.get("schemaVersion") != 1: fail("release lock does not match schema version 1") if lock["package"] != {"name": "c2pa-python", "version": RELEASE_VERSION}: - fail("release lock package identity is not 0.37.13.dev1") + fail("release lock package identity is not 0.37.13.dev2") if lock["pythonSource"] != { "repository": "castlabs/c2pa-python", "url": "https://github.com/castlabs/c2pa-python.git", "releaseBranch": "feat/trusted-vsi-functional", - "releaseTag": "castlabs-v0.37.13.dev1", + "releaseTag": "castlabs-v0.37.13.dev2", }: fail("unexpected c2pa-python release source policy") rust = lock["rustSource"] diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 57f61898..654330ab 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -11,7 +11,7 @@ # specific language governing permissions and limitations under # each license. -# Version: 0.37.13.dev1 +# Version: 0.37.13.dev2 import ctypes import enum @@ -4843,8 +4843,9 @@ def detailed_json(self) -> str: def crjson(self) -> str: """Get the manifest store as a crJSON string. - crJSON is a standardized JSON format for C2PA manifest data. This - call yields empty JSON ("{}") when there are no Content Credentials. + crJSON is a standardized JSON format for C2PA manifest data. The + native export is checked: exporter failures raise instead of + returning an empty ``{}`` document. Returns: The manifest store as a crJSON string. diff --git a/tests/test_castlabs_release_smoke.py b/tests/test_castlabs_release_smoke.py index 95c1fd33..8dd940a6 100644 --- a/tests/test_castlabs_release_smoke.py +++ b/tests/test_castlabs_release_smoke.py @@ -46,7 +46,7 @@ FIXTURES = Path(__file__).parent / "fixtures" -EXPECTED_VERSION = os.environ.get("CASTLABS_RELEASE_EXPECTED_VERSION", "0.37.13.dev1") +EXPECTED_VERSION = os.environ.get("CASTLABS_RELEASE_EXPECTED_VERSION", "0.37.13.dev2") REQUIRED_CAPABILITIES = { "dynamic assertions": has_dynamic_assertions, "fragmented files": has_fragmented_files, diff --git a/tests/test_castlabs_release_tooling.py b/tests/test_castlabs_release_tooling.py index 6f160c4e..09f7cadf 100644 --- a/tests/test_castlabs_release_tooling.py +++ b/tests/test_castlabs_release_tooling.py @@ -50,10 +50,10 @@ def _posix_bash() -> str: def test_prerelease_version_is_consistent(): # The release identity is consistent across the tooling, lock and source, - # so validate-sources accepts this checkout under the 0.37.13.dev1 lock. - assert release.RELEASE_VERSION == "0.37.13.dev1" - assert release.RELEASE_TAG == "castlabs-v0.37.13.dev1" - assert release.load_lock()["package"]["version"] == "0.37.13.dev1" + # so validate-sources accepts this checkout under the 0.37.13.dev2 lock. + assert release.RELEASE_VERSION == "0.37.13.dev2" + assert release.RELEASE_TAG == "castlabs-v0.37.13.dev2" + assert release.load_lock()["package"]["version"] == "0.37.13.dev2" source = release.project_version(ROOT) assert source == release.RELEASE_VERSION first_line = ( @@ -68,7 +68,7 @@ def test_release_lock_and_schemas_are_valid_json(): assert lock["rustSource"]["commit"] == release.RUST_COMMIT assert lock["rustSource"]["cargoLockSha256"] == release.CARGO_LOCK_SHA256 assert lock["rustToolchain"]["channel"] == "1.96.0" - assert release.RUST_COMMIT == "3328b2b440fd07d9f857cbefa3c6d565093425de" + assert release.RUST_COMMIT == "afd6fffe5a066e0b5b1a14f3f8c7ac189e7f0c95" assert release.CARGO_LOCK_SHA256 == ( "f7022b3e05c2dc2080946064b386b293b29c3a60ca9daf344e1b4e3fc398c370" ) @@ -195,7 +195,7 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( in release_workflow ) assert ( - "c2pa_python-0.37.13.dev1-py3-none-manylinux_2_28_x86_64.whl" in release_workflow + "c2pa_python-0.37.13.dev2-py3-none-manylinux_2_28_x86_64.whl" in release_workflow ) assert release_workflow.count("verify-wheel-native") == 2 assert release_workflow.index("--only-plat --plat manylinux_2_28_x86_64") < ( @@ -246,7 +246,7 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( "python3 -m pytest -q c2pa-python/tests/test_castlabs_release_tooling.py" in (release_workflow) ) - assert "if: github.ref == 'refs/tags/castlabs-v0.37.13.dev1'" in release_workflow + assert "if: github.ref == 'refs/tags/castlabs-v0.37.13.dev2'" in release_workflow assert "-F draft=true -F prerelease=true" in release_workflow assert "--clobber" not in release_workflow assert "repos/castlabs/c2pa-python/releases" in release_workflow @@ -320,9 +320,9 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( in pypi_workflow ) assert '--signer-digest "$SOURCE_SHA"' in pypi_workflow - assert "--source-ref refs/tags/castlabs-v0.37.13.dev1" in pypi_workflow + assert "--source-ref refs/tags/castlabs-v0.37.13.dev2" in pypi_workflow assert '--source-digest "$SOURCE_SHA"' in pypi_workflow - assert "pypi.org/pypi/c2pa-python/0.37.13.dev1/json" in pypi_workflow + assert "pypi.org/pypi/c2pa-python/0.37.13.dev2/json" in pypi_workflow assert "pypi-plan" in pypi_workflow assert "packages-dir: publish-dist/" in pypi_workflow assert "if: steps.pypi.outputs.upload == 'true'" in pypi_workflow @@ -351,7 +351,7 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( assert "manual legacy publishing accepts final X.Y.Z versions only" in ( legacy_release ) - assert 'test "$VERSION" != 0.37.13.dev1' not in legacy_workflow + assert 'test "$VERSION" != 0.37.13.dev2' not in legacy_workflow assert legacy_workflow.count("final X.Y.Z versions only") == 2 assert "tests/test_castlabs_release_tooling.py" in legacy_workflow shell_helpers_start = release_workflow.index(" download_existing() {") @@ -381,8 +381,22 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( assert smoke_gate in smoke assert smoke.index(smoke_gate) < smoke.index("from c2pa import") assert release_workflow.count('CASTLABS_RELEASE_SMOKE_REQUIRED: "1"') == 2 - assert release_workflow.count("CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev1") == 2 + assert release_workflow.count("CASTLABS_RELEASE_EXPECTED_VERSION: 0.37.13.dev2") == 2 assert release_workflow.count("C2PA_SOURCE_BUILD_VERSION: 0.92.0-dev") == 2 + # pytest is not a wheel runtime dependency: both clean installed-wheel + # environments install it after the wheel, before running pytest. + linux_install = '"${RUNNER_TEMP}/wheel-test/bin/python" -m pip install release-out/*.whl\n' + linux_pytest = '"${RUNNER_TEMP}/wheel-test/bin/python" -m pip install pytest==8.4.1\n' + windows_install = '& "$env:RUNNER_TEMP\\wheel-test\\Scripts\\python.exe" -m pip install $wheel\n' + windows_pytest = '& "$env:RUNNER_TEMP\\wheel-test\\Scripts\\python.exe" -m pip install pytest==8.4.1\n' + for install, pytest_install, run in ( + (linux_install, linux_pytest, '"${RUNNER_TEMP}/wheel-test/bin/python" -m pytest'), + (windows_install, windows_pytest, '& "$env:RUNNER_TEMP\\wheel-test\\Scripts\\python.exe" -m pytest'), + ): + assert release_workflow.count(pytest_install) == 1 + assert (release_workflow.index(install) + < release_workflow.index(pytest_install) + < release_workflow.index(run)) # The pinned native exports offset-aware fragment verification. assert '"fragment at offset": has_fragment_at_offset,' in smoke assert "pytest.skip(" in smoke @@ -431,6 +445,9 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( assert "castlabs-v0.37.8.dev3" not in pypi_workflow assert "castlabs-v0.37.8.dev4" not in release_workflow assert "castlabs-v0.37.8.dev4" not in pypi_workflow + # The unpublished dev1 tag is never a trigger, gate or source ref again. + assert "0.37.13.dev1" not in release_workflow + assert "0.37.13.dev1" not in pypi_workflow readme = (ROOT / "README.md").read_text(encoding="utf-8") assert "failed prerelease workflow run `34030865864`" in readme assert "No dev1 draft or GitHub release was created" in readme @@ -447,7 +464,8 @@ def test_release_workflows_are_pinned_bounded_and_do_not_drift_from_helper( release_notes = (ROOT / "docs" / "release-notes.md").read_text( encoding="utf-8" ) - assert "## Version 0.37.13.dev1" in release_notes + assert "## Version 0.37.13.dev2" in release_notes + assert "### 0.37.13.dev1 (not released)" in release_notes assert "Dev4 and earlier accepted\n undersized callback outputs" in release_notes assert "breaking strictness\n change" in release_notes @@ -642,7 +660,7 @@ def test_safe_extract_round_trip_and_rejects_links(): def test_wheel_inspection_rejects_unsafe_and_multiple_native_members(): with tempfile.TemporaryDirectory() as temp: root = Path(temp) - wheel = root / "c2pa_python-0.37.13.dev1-py3-none-win_amd64.whl" + wheel = root / "c2pa_python-0.37.13.dev2-py3-none-win_amd64.whl" with zipfile.ZipFile(wheel, "w") as archive: archive.writestr("c2pa/libs/first.dll", b"one") archive.writestr("c2pa/libs/second.dll", b"two") @@ -653,7 +671,7 @@ def test_wheel_inspection_rejects_unsafe_and_multiple_native_members(): else: raise AssertionError("accepted wheel with multiple native libraries") - unsafe = root / "c2pa_python-0.37.13.dev1-py3-none-manylinux_2_28_x86_64.whl" + unsafe = root / "c2pa_python-0.37.13.dev2-py3-none-manylinux_2_28_x86_64.whl" with zipfile.ZipFile(unsafe, "w") as archive: archive.writestr("../libc2pa_c.so", b"unsafe") try: diff --git a/tests/test_trusted_vsi_build.py b/tests/test_trusted_vsi_build.py index 704a43a0..587a4161 100644 --- a/tests/test_trusted_vsi_build.py +++ b/tests/test_trusted_vsi_build.py @@ -15,7 +15,7 @@ SPEC.loader.exec_module(functional_build) -@pytest.mark.parametrize("version", ["0.37.13.dev1", "0.37.13.dev2", "0.38.0.dev1", "1.0.dev0+local"]) +@pytest.mark.parametrize("version", ["0.37.13.dev2", "0.37.13.dev3", "0.38.0.dev1", "1.0.dev0+local"]) def test_new_functional_development_version(version): assert functional_build.functional_version(version) == version @@ -24,7 +24,9 @@ def test_new_functional_development_version(version): "0.37.8.dev5", "0.37.8.dev6", "0.37.8.dev5+functional", "0.37.7.dev1", "0.37.9", "0.38.0rc1", "invalid", # Historical functional qualification identities are older than the source. - "0.37.9.dev0", "0.37.12.dev0", "0.37.13.dev0", + "0.37.9.dev0", "0.37.12.dev0", "0.37.13.dev0", "0.37.13.dev1", + # Conformance-line prereleases merged into this line are older too. + "0.37.12.dev4", ]) def test_functional_build_rejects_release_or_dev5_identity(version): with pytest.raises(ValueError): @@ -39,9 +41,9 @@ def test_staged_sdist_uses_new_version_without_touching_dev5_checkout(tmp_path): before = {name: (ROOT / name).read_bytes() for name in ("pyproject.toml", "src/c2pa/c2pa.py")} stage = tmp_path / "stage" stage.mkdir() - functional_build.stage_source(stage, "0.37.13.dev2") - assert 'version = "0.37.13.dev2"' in (stage / "pyproject.toml").read_text() - assert "# Version: 0.37.13.dev2" in (stage / "src/c2pa/c2pa.py").read_text() + functional_build.stage_source(stage, "0.37.13.dev3") + assert 'version = "0.37.13.dev3"' in (stage / "pyproject.toml").read_text() + assert "# Version: 0.37.13.dev3" in (stage / "src/c2pa/c2pa.py").read_text() assert not (stage / "src/c2pa/libs").exists() subprocess.run([sys.executable, "setup.py", "-q", "sdist", "--dist-dir", str(tmp_path / "dist")], cwd=stage, check=True, capture_output=True) @@ -50,12 +52,12 @@ def test_staged_sdist_uses_new_version_without_touching_dev5_checkout(tmp_path): (sdist,) = (tmp_path / "dist").iterdir() name, version = parse_sdist_filename(sdist.name) assert canonicalize_name(name) == "c2pa-python" - assert version == Version("0.37.13.dev2") + assert version == Version("0.37.13.dev3") root = sdist.name[:-len(".tar.gz")] with tarfile.open(sdist) as archive: names = archive.getnames() info = archive.extractfile(f"{root}/PKG-INFO").read().decode() - assert "Version: 0.37.13.dev2" in info.splitlines() + assert "Version: 0.37.13.dev3" in info.splitlines() for member in ("scripts/build_trusted_vsi_functional.py", "scripts/qualify_trusted_vsi_functional.py", "docs/trusted-vsi-python-contract.md", "src/c2pa/c2pa.py"): @@ -64,13 +66,13 @@ def test_staged_sdist_uses_new_version_without_touching_dev5_checkout(tmp_path): after = {name: (ROOT / name).read_bytes() for name in before} assert after == before # The checkout keeps its own source identity; staging never rewrites it. - assert 'version = "0.37.13.dev1"' in before["pyproject.toml"].decode() + assert 'version = "0.37.13.dev2"' in before["pyproject.toml"].decode() def test_default_build_version_is_the_source_identity(monkeypatch): monkeypatch.delenv("FUNCTIONAL_BUILD_VERSION", raising=False) - assert functional_build.source_version() == "0.37.13.dev1" - assert functional_build.functional_version(functional_build.source_version()) == "0.37.13.dev1" + assert functional_build.source_version() == "0.37.13.dev2" + assert functional_build.functional_version(functional_build.source_version()) == "0.37.13.dev2" def test_probe_script_requires_every_functional_capability(): diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 4ab20d64..210fa2fe 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -11181,5 +11181,57 @@ def test_ed25519_sign_with_empty_data_raises(self): c2pa_module.ed25519_sign(b"", "not a key") +# Validation time and crJSON 2.4 exist only in the Castlabs source-built native +# (C2PA_SOURCE_BUILD_VERSION set by the paired/release jobs), not in the upstream +# prebuilt library that build.yml downloads. +_CASTLABS_SOURCE_NATIVE = bool(os.environ.get("C2PA_SOURCE_BUILD_VERSION")) + + +class TestConformanceValidationSettings(unittest.TestCase): + """Native caller-controlled validation time and checked crJSON.""" + + @unittest.skipUnless(_CASTLABS_SOURCE_NATIVE, "requires the Castlabs source-built native") + def test_invalid_validation_time_is_rejected(self): + with self.assertRaises(Exception): + Context.from_dict({"verify": {"validation_time": "2027-01-15 08:00:00"}}) + + @unittest.skipUnless(_CASTLABS_SOURCE_NATIVE, "requires the Castlabs source-built native") + def test_validation_time_is_reported_in_crjson(self): + context = Context.from_dict({ + "verify": {"validation_time": "2027-01-15T10:00:00+02:00"}, + }) + fixture = os.path.join(FIXTURES_DIR, "C.jpg") + with open(fixture, "rb") as stream: + reader = Reader("image/jpeg", stream, context=context) + document = json.loads(reader.crjson()) + results = document["manifests"][0]["validationResults"] + self.assertEqual(results["validationTime"], "2027-01-15T08:00:00Z") + # crJSON 2.4: the validator version, and no post-2.4 manifest fields. + self.assertEqual(results["specVersion"], "2.4.0") + for manifest in document["manifests"]: + self.assertNotIn("isUpdateManifest", manifest) + self.assertNotIn("isCompressedManifest", manifest) + + def test_crjson_null_export_raises_native_error(self): + # The checked native export returns NULL plus an error instead of "{}". + with Reader(DEFAULT_TEST_FILE) as reader: + with patch.object(c2pa_module._lib, "c2pa_reader_crjson", + return_value=None), \ + patch.object(c2pa_module, "_read_native_error", + return_value="Other: crJSON export failed"): + with self.assertRaises(Error) as raised: + reader.crjson() + self.assertIn("crJSON export failed", str(raised.exception)) + + def test_crjson_null_export_without_native_error_raises(self): + with Reader(DEFAULT_TEST_FILE) as reader: + with patch.object(c2pa_module._lib, "c2pa_reader_crjson", + return_value=None), \ + patch.object(c2pa_module, "_read_native_error", + return_value=None): + with self.assertRaises(Error): + reader.crjson() + + if __name__ == '__main__': unittest.main(warnings='ignore')