floorvault 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- floorvault-0.1.0/.clusterfuzzlite/Dockerfile +9 -0
- floorvault-0.1.0/.clusterfuzzlite/build.sh +15 -0
- floorvault-0.1.0/.clusterfuzzlite/project.yaml +1 -0
- floorvault-0.1.0/.dockerignore +5 -0
- floorvault-0.1.0/.gitattributes +18 -0
- floorvault-0.1.0/.github/copilot-instructions.md +67 -0
- floorvault-0.1.0/.github/workflows/README-cbom-workflow-removed-2026-09-15.md +27 -0
- floorvault-0.1.0/.github/workflows/cflite-prune.yml +43 -0
- floorvault-0.1.0/.github/workflows/cflite.yml +60 -0
- floorvault-0.1.0/.github/workflows/ci.yml +236 -0
- floorvault-0.1.0/.github/workflows/release.yml +139 -0
- floorvault-0.1.0/.github/workflows/scorecard.yml +59 -0
- floorvault-0.1.0/.github/workflows/verify.yml +52 -0
- floorvault-0.1.0/.gitignore +10 -0
- floorvault-0.1.0/.gitleaks.toml +30 -0
- floorvault-0.1.0/.gitleaksignore +29 -0
- floorvault-0.1.0/LICENSE +15 -0
- floorvault-0.1.0/LICENSE-APACHE +202 -0
- floorvault-0.1.0/LICENSE-MIT +19 -0
- floorvault-0.1.0/PKG-INFO +465 -0
- floorvault-0.1.0/README.md +418 -0
- floorvault-0.1.0/SECURITY.md +583 -0
- floorvault-0.1.0/docs/ARCHITECTURE-REVIEW-2026-10-02.md +190 -0
- floorvault-0.1.0/docs/COMPARATIVE-BENCHMARK-2026-09-15.md +86 -0
- floorvault-0.1.0/docs/CROSS-PLATFORM-CI-FINDINGS-2026-09-15.md +144 -0
- floorvault-0.1.0/docs/DEEP-DIVE-REVIEW-2026-10-01.md +156 -0
- floorvault-0.1.0/docs/FLOORVAULT-ARCHITECTURE-CRYPTO-REPORT-2026-09-16.md +126 -0
- floorvault-0.1.0/docs/HARDWARE-CUSTODY.md +119 -0
- floorvault-0.1.0/docs/PENTEST-2026-10-04.md +118 -0
- floorvault-0.1.0/docs/PERFORMANCE-HARDENING-COST-REVIEW-2026-09-15.md +42 -0
- floorvault-0.1.0/docs/RECORD-FORMAT-2026-09-15.md +127 -0
- floorvault-0.1.0/docs/RELEASING.md +58 -0
- floorvault-0.1.0/docs/REMEDIATION-REPORT-2026-09-16.md +263 -0
- floorvault-0.1.0/docs/SPEC.md +864 -0
- floorvault-0.1.0/docs/VAULT-TRANSIT.md +130 -0
- floorvault-0.1.0/docs/assets/context-binding.svg +48 -0
- floorvault-0.1.0/docs/assets/floorvault-hero.svg +8 -0
- floorvault-0.1.0/fuzz/fuzz_associated_data.py +74 -0
- floorvault-0.1.0/fuzz/fuzz_beacons.py +85 -0
- floorvault-0.1.0/fuzz/fuzz_envelope.py +89 -0
- floorvault-0.1.0/fuzz/fuzz_identifiers.py +82 -0
- floorvault-0.1.0/fuzz/fuzz_protected_store.py +96 -0
- floorvault-0.1.0/fuzz/write_seed_corpus.py +37 -0
- floorvault-0.1.0/pyproject.toml +128 -0
- floorvault-0.1.0/scripts/benchmark_compare.py +126 -0
- floorvault-0.1.0/scripts/gen_vectors.py +628 -0
- floorvault-0.1.0/scripts/memory_probe.py +61 -0
- floorvault-0.1.0/scripts/mutation_check.py +1471 -0
- floorvault-0.1.0/scripts/realworld_exercise.py +843 -0
- floorvault-0.1.0/scripts/security-check.sh +216 -0
- floorvault-0.1.0/scripts/verify_wheel.py +62 -0
- floorvault-0.1.0/src/floorvault/__init__.py +100 -0
- floorvault-0.1.0/src/floorvault/beacons.py +406 -0
- floorvault-0.1.0/src/floorvault/core.py +771 -0
- floorvault-0.1.0/src/floorvault/inspector.py +95 -0
- floorvault-0.1.0/src/floorvault/key_recovery.py +95 -0
- floorvault-0.1.0/src/floorvault/keyring.py +171 -0
- floorvault-0.1.0/src/floorvault/libsql_adapter.py +83 -0
- floorvault-0.1.0/src/floorvault/memory.py +304 -0
- floorvault-0.1.0/src/floorvault/migration.py +357 -0
- floorvault-0.1.0/src/floorvault/platform_support.py +244 -0
- floorvault-0.1.0/src/floorvault/providers/__init__.py +17 -0
- floorvault-0.1.0/src/floorvault/providers/adaptive.py +281 -0
- floorvault-0.1.0/src/floorvault/providers/base.py +42 -0
- floorvault-0.1.0/src/floorvault/providers/generation_store.py +413 -0
- floorvault-0.1.0/src/floorvault/providers/linux_keyring.py +301 -0
- floorvault-0.1.0/src/floorvault/providers/platform_custody.py +429 -0
- floorvault-0.1.0/src/floorvault/providers/vault_transit.py +559 -0
- floorvault-0.1.0/src/floorvault/providers/windows_dpapi.py +249 -0
- floorvault-0.1.0/src/floorvault/records.py +209 -0
- floorvault-0.1.0/src/floorvault/sqlalchemy_adapter.py +489 -0
- floorvault-0.1.0/src/floorvault/sqlite_adapter.py +324 -0
- floorvault-0.1.0/src/floorvault/sqlite_migration.py +241 -0
- floorvault-0.1.0/src/floorvault/vault_rotation.py +115 -0
- floorvault-0.1.0/src/floorvault/vaultkit/__init__.py +28 -0
- floorvault-0.1.0/src/floorvault/vaultkit/session_crypto.py +112 -0
- floorvault-0.1.0/src/floorvault/vaultkit/vault.py +1014 -0
- floorvault-0.1.0/tests/test_adaptive_provider.py +655 -0
- floorvault-0.1.0/tests/test_benchmark.py +57 -0
- floorvault-0.1.0/tests/test_caller_key_lifecycle.py +39 -0
- floorvault-0.1.0/tests/test_ci_workflow.py +273 -0
- floorvault-0.1.0/tests/test_crypto_core.py +343 -0
- floorvault-0.1.0/tests/test_encrypted_sqlite_table.py +135 -0
- floorvault-0.1.0/tests/test_envelope_versioning.py +221 -0
- floorvault-0.1.0/tests/test_fuzz.py +151 -0
- floorvault-0.1.0/tests/test_fuzz_targets.py +129 -0
- floorvault-0.1.0/tests/test_gate_script.py +149 -0
- floorvault-0.1.0/tests/test_generation_store.py +308 -0
- floorvault-0.1.0/tests/test_inspector_sql_injection.py +123 -0
- floorvault-0.1.0/tests/test_key_recovery.py +67 -0
- floorvault-0.1.0/tests/test_key_store_paths.py +244 -0
- floorvault-0.1.0/tests/test_keychain_live.py +70 -0
- floorvault-0.1.0/tests/test_keyring.py +199 -0
- floorvault-0.1.0/tests/test_latency_hotpath.py +274 -0
- floorvault-0.1.0/tests/test_libsql_adapter.py +284 -0
- floorvault-0.1.0/tests/test_linux_keyring.py +607 -0
- floorvault-0.1.0/tests/test_memory_custody.py +175 -0
- floorvault-0.1.0/tests/test_memory_platform_probe.py +63 -0
- floorvault-0.1.0/tests/test_migration.py +180 -0
- floorvault-0.1.0/tests/test_migration_retirement.py +216 -0
- floorvault-0.1.0/tests/test_mutation_expectations.py +157 -0
- floorvault-0.1.0/tests/test_platform_providers.py +125 -0
- floorvault-0.1.0/tests/test_platform_support.py +163 -0
- floorvault-0.1.0/tests/test_properties.py +96 -0
- floorvault-0.1.0/tests/test_protected_store_safety.py +928 -0
- floorvault-0.1.0/tests/test_public_api.py +267 -0
- floorvault-0.1.0/tests/test_realworld_exercise.py +62 -0
- floorvault-0.1.0/tests/test_release_artifacts.py +60 -0
- floorvault-0.1.0/tests/test_repository_hygiene.py +123 -0
- floorvault-0.1.0/tests/test_revision_binding.py +117 -0
- floorvault-0.1.0/tests/test_rfc5297_independent.py +409 -0
- floorvault-0.1.0/tests/test_rfc_vectors.py +73 -0
- floorvault-0.1.0/tests/test_rotation.py +237 -0
- floorvault-0.1.0/tests/test_scrub_secrets.py +66 -0
- floorvault-0.1.0/tests/test_searchable_beacons.py +687 -0
- floorvault-0.1.0/tests/test_security_scan.py +216 -0
- floorvault-0.1.0/tests/test_session_crypto_framing.py +135 -0
- floorvault-0.1.0/tests/test_sql_identifier_quoting.py +219 -0
- floorvault-0.1.0/tests/test_sqlalchemy_adapter.py +494 -0
- floorvault-0.1.0/tests/test_sqlite_integration.py +76 -0
- floorvault-0.1.0/tests/test_sqlite_migration.py +230 -0
- floorvault-0.1.0/tests/test_store_permissions.py +243 -0
- floorvault-0.1.0/tests/test_vault_rotation.py +307 -0
- floorvault-0.1.0/tests/test_vault_transit.py +601 -0
- floorvault-0.1.0/tests/test_vault_transit_live.py +69 -0
- floorvault-0.1.0/tests/test_vaultkit_adapter.py +365 -0
- floorvault-0.1.0/tests/test_vaultstore_permissions.py +34 -0
- floorvault-0.1.0/tests/test_verify_wheel.py +56 -0
- floorvault-0.1.0/tests/test_windows_dpapi_live.py +20 -0
- floorvault-0.1.0/tests/test_wire_vectors.py +371 -0
- floorvault-0.1.0/tests/test_wycheproof_vectors.py +170 -0
- floorvault-0.1.0/tests/vectors/floorvault_wire_vectors.json +934 -0
- floorvault-0.1.0/tests/vectors/independent_wire.py +569 -0
- floorvault-0.1.0/tests/vectors/wycheproof/LICENSE +202 -0
- floorvault-0.1.0/tests/vectors/wycheproof/aead_aes_siv_cmac_test.json +12809 -0
- floorvault-0.1.0/tests/vectors/wycheproof/aes_siv_cmac_test.json +4203 -0
- floorvault-0.1.0/uv.lock +1534 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# ClusterFuzzLite build image for FloorVault's Atheris fuzz targets (fuzz/).
|
|
2
|
+
#
|
|
3
|
+
# Pinned by digest: the base image carries the fuzzing toolchain (Atheris,
|
|
4
|
+
# libFuzzer, the sanitizer runtimes), so a moving tag would let the thing that
|
|
5
|
+
# decides "no crash found" change underneath an unchanged repository.
|
|
6
|
+
FROM gcr.io/oss-fuzz-base/base-builder-python@sha256:58f80c90a90944b68b8e70b98e38997ee9396225311cfb0f3aea78671446ab4e
|
|
7
|
+
COPY . $SRC/floorvault
|
|
8
|
+
WORKDIR $SRC/floorvault
|
|
9
|
+
COPY .clusterfuzzlite/build.sh $SRC/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash -eu
|
|
2
|
+
# Build every Atheris fuzz target in fuzz/ for ClusterFuzzLite.
|
|
3
|
+
#
|
|
4
|
+
# Installs FloorVault from this checkout (not from PyPI), so the fuzzers exercise
|
|
5
|
+
# the code under review, then packages each target with the seed corpus it
|
|
6
|
+
# declares in seed_inputs(). A target added to fuzz/ is picked up automatically;
|
|
7
|
+
# tests/test_fuzz_targets.py pins that this glob covers them all.
|
|
8
|
+
|
|
9
|
+
pip3 install --no-cache-dir "$SRC/floorvault"
|
|
10
|
+
|
|
11
|
+
for fuzzer in "$SRC"/floorvault/fuzz/fuzz_*.py; do
|
|
12
|
+
name="$(basename -s .py "$fuzzer")"
|
|
13
|
+
compile_python_fuzzer "$fuzzer"
|
|
14
|
+
python3 "$SRC/floorvault/fuzz/write_seed_corpus.py" "$fuzzer" "$OUT/${name}_seed_corpus.zip"
|
|
15
|
+
done
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
language: python
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Normalise line endings for every text file in the repository.
|
|
2
|
+
#
|
|
3
|
+
# Without this the checked-out bytes depend on the platform: git's Windows
|
|
4
|
+
# default (core.autocrlf=true, which is also what the GitHub Windows runners use)
|
|
5
|
+
# rewrites text files to CRLF. The build then packs those CRLF bytes, so the
|
|
6
|
+
# wheel and sdist differ from a POSIX build of the same commit - two digests for
|
|
7
|
+
# one source tree, and no way to check a download against a build from source.
|
|
8
|
+
#
|
|
9
|
+
# eol=lf forces LF on checkout everywhere, so the artifact is at least a function
|
|
10
|
+
# of the source rather than of the machine that checked it out.
|
|
11
|
+
#
|
|
12
|
+
# What this does NOT fix: the archive records the file mode it sees on the
|
|
13
|
+
# filesystem (measured: a source file chmod 0755 produces a different wheel than
|
|
14
|
+
# the same file at 0644), and Windows cannot represent POSIX modes. Byte-identity
|
|
15
|
+
# *across* operating systems is therefore not claimed anywhere; the digest
|
|
16
|
+
# identifies the artifact for a given platform. See tests/test_release_artifacts.py
|
|
17
|
+
# and Appendix B of the third-party review report.
|
|
18
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copilot review instructions — FloorVault
|
|
2
|
+
|
|
3
|
+
FloorVault is contextual, misuse-resistant database encryption (AES-256-SIV
|
|
4
|
+
with coordinate-bound AAD). Reviews here are security reviews first, style
|
|
5
|
+
reviews second.
|
|
6
|
+
|
|
7
|
+
## Hard rules — flag any violation
|
|
8
|
+
|
|
9
|
+
- **Never weaken authentication.** The AEAD associated-data vector is
|
|
10
|
+
`[aad, header, nonce]` for v2 envelopes and `[aad, nonce]` for v1. Every
|
|
11
|
+
cleartext envelope field must stay inside that vector. Removing an element,
|
|
12
|
+
reordering, or mutating cleartext fields is a critical finding.
|
|
13
|
+
- **Never remove or weaken validation.** Rejection rules in `docs/SPEC.md`
|
|
14
|
+
§4.3/§16 are normative, including their ORDER. Do not suggest relaxing a
|
|
15
|
+
check for convenience.
|
|
16
|
+
- **Nonce discipline.** Nonces are 16 bytes from `os.urandom`, fresh per
|
|
17
|
+
field. Counter-derived, reused, or truncated nonces are critical findings.
|
|
18
|
+
The dedup window is defence-in-depth, not a freshness mechanism.
|
|
19
|
+
- **No key/custody downgrades.** A broken or refused custody tier must raise,
|
|
20
|
+
never silently fall through to a weaker one. No-clobber store publication
|
|
21
|
+
(hard link) is deliberate; do not suggest `os.replace`.
|
|
22
|
+
- **SQL identifiers** must pass `_safe_identifier` and be interpolated via
|
|
23
|
+
`_quoted_identifier` (bracket-quoted). Any other interpolation of a name
|
|
24
|
+
into SQL is a finding.
|
|
25
|
+
- **On-disk formats are a contract** (`docs/SPEC.md`). Byte-level changes
|
|
26
|
+
require a version bump AND a reader for every documented version. Flag any
|
|
27
|
+
wire-format change without a spec + vector update.
|
|
28
|
+
- **Tombstones/retirement** are fail-closed: a migrated id is never served
|
|
29
|
+
from the legacy source again, even if the modern record is gone.
|
|
30
|
+
|
|
31
|
+
## Conventions
|
|
32
|
+
|
|
33
|
+
- Errors are honest: no swallowed exceptions, no "cannot tell is fine", no
|
|
34
|
+
masking one check behind another (the mutation suite hunts these).
|
|
35
|
+
- New security guards get a curated mutant in `scripts/mutation_check.py`;
|
|
36
|
+
platform-equivalent survivors are declared, not silently skipped.
|
|
37
|
+
- Comments and docstrings explain *why*, and state limits plainly. Correct an
|
|
38
|
+
outdated claim rather than leave it — this codebase treats wrong docs as
|
|
39
|
+
bugs (see `docs/SPEC.md` §17).
|
|
40
|
+
- `json.dumps` for authenticated data uses `ensure_ascii=False`,
|
|
41
|
+
`sort_keys=True`, `separators=(",", ":")`; payload JSON uses default
|
|
42
|
+
separators. They are NOT interchangeable.
|
|
43
|
+
- Style: ruff clean, compact, no unnecessary try/except — error boundaries
|
|
44
|
+
follow the existing pattern.
|
|
45
|
+
|
|
46
|
+
## Frozen quirks — flag these too, and weigh them in every review
|
|
47
|
+
|
|
48
|
+
These behaviors are deliberate *today* (documented and frozen in
|
|
49
|
+
`docs/SPEC.md`), but they are not above scrutiny. Flag them when they appear
|
|
50
|
+
in a diff or are relevant to a change, note that the design intent is known,
|
|
51
|
+
and assess whether the change under review makes the quirk riskier — e.g.
|
|
52
|
+
new code that relies on one of these edge cases is worth a comment even
|
|
53
|
+
though the behavior itself is intentional:
|
|
54
|
+
|
|
55
|
+
- `memoryview` plaintext input is refused; `bytearray` envelope input is
|
|
56
|
+
snapshotted once before parsing (§4.3). Intentional — snapshot safety.
|
|
57
|
+
- Negative or >64-bit `schema_version`/`revision` integers serialize into
|
|
58
|
+
the AAD (§5.2). Frozen quirk — any tightening must be write-side only so
|
|
59
|
+
existing records stay readable.
|
|
60
|
+
- `record_id=" r "` (whitespace-padded) is accepted verbatim; only
|
|
61
|
+
whitespace-only strings are refused (frozen).
|
|
62
|
+
- Integer subclasses in coordinate ints are serialized via `int.__repr__`,
|
|
63
|
+
not `str()` — the fast path exists specifically so a subclass cannot
|
|
64
|
+
inject bytes into the AAD. Any new serialization path must keep that.
|
|
65
|
+
- Nonce dedup is in-process only (deque+set window): it does not survive
|
|
66
|
+
restart and is not a freshness mechanism — `revision` bound into the AAD
|
|
67
|
+
is. Flag anything that treats the window as stronger.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Crypto Bill of Materials workflow — removed 2026-09-15
|
|
2
|
+
|
|
3
|
+
The `.github/workflows/main.yml` "Create Crypto Bill of Materials" workflow,
|
|
4
|
+
added in commit `7ca3499`, was removed in `18de92f` (this file's parent).
|
|
5
|
+
|
|
6
|
+
**Reason.** The workflow consumed `advanced-security/cbom-action@v1`, which
|
|
7
|
+
internally uses the now-deprecated `actions/upload-artifact: v3`. GitHub
|
|
8
|
+
auto-blocks requests to that deprecated action, so every matrix-analysis job
|
|
9
|
+
(and therefore the whole workflow) failed on every run, including the first
|
|
10
|
+
run after the workflow was added and again on the push that included this
|
|
11
|
+
removal. The failure was in the third-party action's own machinery, not in
|
|
12
|
+
anything `vaultfloor/floorvault` ships.
|
|
13
|
+
|
|
14
|
+
**What the workflow did.** It used the GitHub Advanced Security CBOM action to
|
|
15
|
+
build a crypto bill of materials across a matrix of public Python ecosystem
|
|
16
|
+
repositories (`coveragepy`, `cryptography`, `ruff`, `packaging`, `pytest`,
|
|
17
|
+
`pyobjc`, etc.), not floorvault's own dependencies in any actionable sense.
|
|
18
|
+
|
|
19
|
+
**What was already covering the real gate.** FloorVault's secret scan is
|
|
20
|
+
gitleaks in `.github/workflows/ci.yml` (CI job `secret scan (history)`), which
|
|
21
|
+
runs on every push to `main` and must pass for the push to be considered
|
|
22
|
+
gate-clean. The CBOM workflow was informational only and was never part of the
|
|
23
|
+
pre-push gate.
|
|
24
|
+
|
|
25
|
+
**Status.** Removed. No replacement is in place; if a crypto bill of materials
|
|
26
|
+
is wanted in future it should use a maintained action whose dependencies do not
|
|
27
|
+
include a deprecated artifact-action version.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: ClusterFuzzLite Corpus Prune
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: [ClusterFuzzLite]
|
|
6
|
+
types: [completed]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
actions: read
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: cflite-prune-main
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
# Corpus pruning is required alongside batch fuzzing: without it the artifact-
|
|
19
|
+
# persisted corpus grows unboundedly. Runs after the weekly batch or by manual dispatch.
|
|
20
|
+
prune:
|
|
21
|
+
name: corpus prune (weekly)
|
|
22
|
+
if: >-
|
|
23
|
+
github.event_name == 'workflow_dispatch' ||
|
|
24
|
+
(github.event.workflow_run.event == 'schedule' &&
|
|
25
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
26
|
+
github.event.workflow_run.head_branch == 'main' &&
|
|
27
|
+
github.event.workflow_run.head_repository.full_name == github.repository)
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- name: Build fuzzers
|
|
31
|
+
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
|
|
32
|
+
with:
|
|
33
|
+
language: python
|
|
34
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
sanitizer: address
|
|
36
|
+
|
|
37
|
+
- name: Prune corpus
|
|
38
|
+
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
|
|
39
|
+
with:
|
|
40
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
sanitizer: address
|
|
42
|
+
mode: prune
|
|
43
|
+
fuzz-seconds: 600
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: ClusterFuzzLite
|
|
2
|
+
|
|
3
|
+
# Coverage-guided fuzzing of the Atheris targets in fuzz/ (envelope parsing,
|
|
4
|
+
# canonical AAD, the protected key-store reader, SQL identifier validation and
|
|
5
|
+
# beacon encoding). Build integration lives in .clusterfuzzlite/.
|
|
6
|
+
#
|
|
7
|
+
# Pull requests fuzz for 5 minutes in code-change mode, so a crash introduced by
|
|
8
|
+
# the PR fails it. Pushes to main and a weekly schedule run longer batch fuzzing.
|
|
9
|
+
# With no storage repo configured, ClusterFuzzLite persists the corpus as GitHub
|
|
10
|
+
# Actions artifacts: each batch run downloads the previous run's corpus-*
|
|
11
|
+
# artifacts (that is what the actions: read permission is for) and uploads the
|
|
12
|
+
# grown corpus. Weekly pruning runs in a separate follow-on workflow
|
|
13
|
+
# (cflite-prune.yml) after the completed batch, so the persisted corpus stays
|
|
14
|
+
# minimized instead of growing unboundedly.
|
|
15
|
+
#
|
|
16
|
+
# The private repository needs the token passed to both steps.
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
pull_request:
|
|
20
|
+
push:
|
|
21
|
+
branches: [main]
|
|
22
|
+
schedule:
|
|
23
|
+
- cron: "41 2 * * 0"
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
fuzz:
|
|
31
|
+
name: fuzz (${{ github.event_name == 'pull_request' && 'code-change' || 'batch' }})
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
# Lets run_fuzzers list previous runs' artifacts (corpus, builds); without
|
|
36
|
+
# it the download fails with 403 and every run starts from the seeds.
|
|
37
|
+
actions: read
|
|
38
|
+
concurrency:
|
|
39
|
+
group: cflite-${{ github.ref }}
|
|
40
|
+
cancel-in-progress: true
|
|
41
|
+
steps:
|
|
42
|
+
- name: Build fuzzers
|
|
43
|
+
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
|
|
44
|
+
with:
|
|
45
|
+
language: python
|
|
46
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
47
|
+
sanitizer: address
|
|
48
|
+
|
|
49
|
+
- name: Run fuzzers
|
|
50
|
+
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
|
|
51
|
+
with:
|
|
52
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
53
|
+
sanitizer: address
|
|
54
|
+
mode: ${{ github.event_name == 'pull_request' && 'code-change' || 'batch' }}
|
|
55
|
+
fuzz-seconds: ${{ github.event_name == 'pull_request' && 300 || 1800 }}
|
|
56
|
+
# Fail on a crash even when the reproduction step cannot confirm it.
|
|
57
|
+
# Python fuzzer start-up is slow enough that reproduction can time out;
|
|
58
|
+
# the first run of this workflow found a real crash, failed to
|
|
59
|
+
# reproduce it for exactly that reason, discarded it, and went green.
|
|
60
|
+
report-unreproducible-crashes: true
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Runs the same security gates developers run before pushing
|
|
4
|
+
# (scripts/security-check.sh) across every supported OS and Python version, so
|
|
5
|
+
# the cross-platform and "zero C compilation" claims are exercised on every
|
|
6
|
+
# push rather than only on a maintainer's machine.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: ci-${{ github.ref }}
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
# Python-version sweep on Linux (regressions here are platform-independent),
|
|
23
|
+
# plus one macOS and one Windows runner for the OS-specific code paths.
|
|
24
|
+
gates:
|
|
25
|
+
name: ${{ matrix.os }} / py${{ matrix.python }}
|
|
26
|
+
runs-on: ${{ matrix.os }}
|
|
27
|
+
strategy:
|
|
28
|
+
fail-fast: false
|
|
29
|
+
matrix:
|
|
30
|
+
include:
|
|
31
|
+
# Linux: the full supported range.
|
|
32
|
+
- { os: ubuntu-latest, python: "3.10" }
|
|
33
|
+
- { os: ubuntu-latest, python: "3.11" }
|
|
34
|
+
- { os: ubuntu-latest, python: "3.12" }
|
|
35
|
+
- { os: ubuntu-latest, python: "3.13" }
|
|
36
|
+
- { os: ubuntu-latest, python: "3.14" }
|
|
37
|
+
# macOS and Windows: BOTH ends of the range, not just the default.
|
|
38
|
+
# A platform-specific code path can be fine on the newest interpreter
|
|
39
|
+
# and broken on the oldest (or the reverse), and every defect the
|
|
40
|
+
# matrix has found so far was platform-specific; see
|
|
41
|
+
# docs/CROSS-PLATFORM-CI-FINDINGS-2026-09-15.md. 3.10 is the floor
|
|
42
|
+
# declared in pyproject.toml, so it must be exercised on every OS.
|
|
43
|
+
- { os: macos-latest, python: "3.10" }
|
|
44
|
+
- { os: macos-latest, python: "3.13" }
|
|
45
|
+
- { os: windows-latest, python: "3.10" }
|
|
46
|
+
- { os: windows-latest, python: "3.13" }
|
|
47
|
+
steps:
|
|
48
|
+
# fetch-depth: 0 is required because gitleaks scans history, not just the
|
|
49
|
+
# tip; a shallow checkout would make the scan meaningless.
|
|
50
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
|
|
51
|
+
with:
|
|
52
|
+
fetch-depth: 0
|
|
53
|
+
|
|
54
|
+
- name: Set up uv and Python ${{ matrix.python }}
|
|
55
|
+
# Pinned to a full version: setup-uv only maintains moving major tags up
|
|
56
|
+
# to v7; v8+ exist as full versions only (v10 does not resolve).
|
|
57
|
+
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
|
|
58
|
+
with:
|
|
59
|
+
enable-cache: true
|
|
60
|
+
python-version: ${{ matrix.python }}
|
|
61
|
+
# Pin the uv binary too: setup-uv otherwise resolves its latest
|
|
62
|
+
# release, so the tool building the artifacts would move even with
|
|
63
|
+
# the action pinned by SHA.
|
|
64
|
+
version: "0.12.5"
|
|
65
|
+
|
|
66
|
+
- name: Sync environment
|
|
67
|
+
shell: bash
|
|
68
|
+
run: |
|
|
69
|
+
if [ "${{ runner.os }}" = "macOS" ]; then
|
|
70
|
+
# Keychain (Tier 2 of the adaptive provider) needs the Security bindings.
|
|
71
|
+
uv sync --extra dev --extra macos
|
|
72
|
+
else
|
|
73
|
+
uv sync --extra dev
|
|
74
|
+
fi
|
|
75
|
+
# The gate audits dependencies with pip-audit; install it into the venv
|
|
76
|
+
# so require_tool finds it under FLOORVAULT_STRICT=1 on every OS.
|
|
77
|
+
uv pip install pip-audit
|
|
78
|
+
|
|
79
|
+
- name: Run security gates
|
|
80
|
+
shell: bash
|
|
81
|
+
env:
|
|
82
|
+
FLOORVAULT_STRICT: "1"
|
|
83
|
+
# History is identical on every runner, so the secret scan runs once,
|
|
84
|
+
# in the `secrets` job below.
|
|
85
|
+
FLOORVAULT_SKIP_SECRET_SCAN: "1"
|
|
86
|
+
# Same reasoning for Semgrep: it runs once, in the `sast` job below.
|
|
87
|
+
FLOORVAULT_SKIP_SEMGREP: "1"
|
|
88
|
+
run: bash scripts/security-check.sh
|
|
89
|
+
|
|
90
|
+
# Publish one leg's artifacts as the release identity. The gate prints the
|
|
91
|
+
# digests of what it built on EVERY leg (step 11), so the other platforms
|
|
92
|
+
# stay comparable from the logs; this step exists so that a released wheel
|
|
93
|
+
# can be checked against a CI-built artifact rather than only against a
|
|
94
|
+
# maintainer's machine. One leg only: ten legs publishing the same name
|
|
95
|
+
# would collide, and upload-artifact cannot merge them meaningfully.
|
|
96
|
+
- name: Publish the built artifacts
|
|
97
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.13'
|
|
98
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
99
|
+
with:
|
|
100
|
+
name: floorvault-dist
|
|
101
|
+
path: |
|
|
102
|
+
dist/*.whl
|
|
103
|
+
dist/*.tar.gz
|
|
104
|
+
# Fail closed: an upload step that silently publishes nothing is worse
|
|
105
|
+
# than no step at all, because it looks like the artifact was captured.
|
|
106
|
+
if-no-files-found: error
|
|
107
|
+
|
|
108
|
+
# Secret scanning over full history, once, on Linux.
|
|
109
|
+
#
|
|
110
|
+
# This runs the gitleaks CLI rather than `gitleaks/gitleaks-action`: the Action
|
|
111
|
+
# requires a licence for ORGANIZATION-owned repositories, and this repository
|
|
112
|
+
# moved into the `vaultfloor` organization, at which point the job started
|
|
113
|
+
# failing with "License key is required" and - because the workflow cancels
|
|
114
|
+
# in-progress runs - cancelled five other legs with it. The CLI has no such
|
|
115
|
+
# gate, runs exactly the command the local gate runs, and removes one
|
|
116
|
+
# third-party action from the supply chain.
|
|
117
|
+
#
|
|
118
|
+
# The download is version-pinned and checksum-verified: a scanner that cannot
|
|
119
|
+
# be verified must not silently become the thing that certifies everything else.
|
|
120
|
+
secrets:
|
|
121
|
+
name: secret scan (history)
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
permissions:
|
|
124
|
+
contents: read
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
|
|
127
|
+
with:
|
|
128
|
+
# A shallow checkout would make a history scan meaningless.
|
|
129
|
+
fetch-depth: 0
|
|
130
|
+
- name: Install gitleaks (pinned, checksum-verified)
|
|
131
|
+
shell: bash
|
|
132
|
+
run: |
|
|
133
|
+
set -euo pipefail
|
|
134
|
+
VERSION=8.30.1
|
|
135
|
+
ASSET="gitleaks_${VERSION}_linux_x64.tar.gz"
|
|
136
|
+
SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
|
|
137
|
+
curl -sSfL -o "$ASSET" \
|
|
138
|
+
"https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${ASSET}"
|
|
139
|
+
echo "${SHA256} ${ASSET}" | sha256sum -c -
|
|
140
|
+
tar -xzf "$ASSET" gitleaks
|
|
141
|
+
sudo install -m 0755 gitleaks /usr/local/bin/gitleaks
|
|
142
|
+
gitleaks version
|
|
143
|
+
- name: Scan history for secrets
|
|
144
|
+
shell: bash
|
|
145
|
+
# Same command as step 1 of scripts/security-check.sh, so CI and a local
|
|
146
|
+
# run agree on what the scan is. Non-zero exit when a secret is found.
|
|
147
|
+
run: gitleaks git --verbose --redact
|
|
148
|
+
|
|
149
|
+
# Static application security testing, once, on Linux.
|
|
150
|
+
#
|
|
151
|
+
# Bandit also runs on every matrix leg inside the gate (it is a dev dependency);
|
|
152
|
+
# it is repeated here so this job is a complete SAST record on its own. Semgrep
|
|
153
|
+
# is pinned to an exact version, but the p/python registry ruleset is fetched
|
|
154
|
+
# at run time and is not versioned: a newly published rule can fail this job
|
|
155
|
+
# without a code change. That is deliberate - a new rule that fires is a new
|
|
156
|
+
# finding to review - and it is why Semgrep runs here rather than on every leg.
|
|
157
|
+
sast:
|
|
158
|
+
name: SAST (Bandit, Semgrep CE)
|
|
159
|
+
runs-on: ubuntu-latest
|
|
160
|
+
permissions:
|
|
161
|
+
contents: read
|
|
162
|
+
steps:
|
|
163
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
|
|
164
|
+
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
|
|
165
|
+
with:
|
|
166
|
+
python-version: "3.13"
|
|
167
|
+
version: "0.12.5"
|
|
168
|
+
- name: Sync environment
|
|
169
|
+
run: uv sync --extra dev
|
|
170
|
+
- name: Bandit
|
|
171
|
+
# Same command as step 3a of scripts/security-check.sh.
|
|
172
|
+
run: uv run bandit -q -r src/ scripts/ fuzz/
|
|
173
|
+
- name: Semgrep CE
|
|
174
|
+
# Same command as step 3b of scripts/security-check.sh.
|
|
175
|
+
run: |
|
|
176
|
+
SEMGREP_VERSION=1.178.0
|
|
177
|
+
uvx --from "semgrep==${SEMGREP_VERSION}" semgrep scan --config p/python --metrics=off --error src/ scripts/ fuzz/
|
|
178
|
+
|
|
179
|
+
# Live-Vault integration leg (roadmap 1f): the hermetic fake-Vault suite can
|
|
180
|
+
# encode a wrong wire assumption in lockstep with the implementation — a real
|
|
181
|
+
# TLS-terminated Vault cannot. This runs the provider end-to-end against a
|
|
182
|
+
# dev server with verified TLS (the provider refuses plain HTTP by design).
|
|
183
|
+
#
|
|
184
|
+
# The Vault binary is version-pinned and checksum-verified for the same
|
|
185
|
+
# reason gitleaks is: a tool certifying the contract must itself be verified.
|
|
186
|
+
# The root token is a throwaway CI literal for an in-memory dev server that
|
|
187
|
+
# dies with the job — it is not a credential.
|
|
188
|
+
vault-live:
|
|
189
|
+
name: vault transit (live dev server)
|
|
190
|
+
runs-on: ubuntu-latest
|
|
191
|
+
steps:
|
|
192
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
|
|
193
|
+
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
|
|
194
|
+
with:
|
|
195
|
+
python-version: "3.13"
|
|
196
|
+
version: "0.12.5"
|
|
197
|
+
- name: Sync environment
|
|
198
|
+
run: uv sync --extra dev
|
|
199
|
+
- name: Install Vault (pinned, checksum-verified)
|
|
200
|
+
run: |
|
|
201
|
+
set -euo pipefail
|
|
202
|
+
VERSION=2.1.1
|
|
203
|
+
ASSET="vault_${VERSION}_linux_amd64.zip"
|
|
204
|
+
SHA256=8aa90f9cea46f541fc7baa3d0ec692fc06afde9a248cc1f2dcac46a567c6f56b
|
|
205
|
+
curl -sSfL -o "$ASSET" \
|
|
206
|
+
"https://releases.hashicorp.com/vault/${VERSION}/${ASSET}"
|
|
207
|
+
echo "${SHA256} ${ASSET}" | sha256sum -c -
|
|
208
|
+
unzip -q "$ASSET" vault
|
|
209
|
+
sudo install -m 0755 vault /usr/local/bin/vault
|
|
210
|
+
vault version
|
|
211
|
+
- name: Start Vault dev server (TLS) and provision the transit key
|
|
212
|
+
run: |
|
|
213
|
+
set -euo pipefail
|
|
214
|
+
mkdir -p "$RUNNER_TEMP/vault-tls"
|
|
215
|
+
nohup vault server -dev -dev-tls \
|
|
216
|
+
-dev-tls-cert-dir="$RUNNER_TEMP/vault-tls" \
|
|
217
|
+
-dev-root-token-id=fv-ci-token \
|
|
218
|
+
-dev-listen-address=127.0.0.1:8200 \
|
|
219
|
+
>"$RUNNER_TEMP/vault-dev.log" 2>&1 &
|
|
220
|
+
export VAULT_ADDR="https://127.0.0.1:8200"
|
|
221
|
+
export VAULT_CACERT="$RUNNER_TEMP/vault-tls/vault-ca.pem"
|
|
222
|
+
export VAULT_TOKEN="fv-ci-token"
|
|
223
|
+
for i in $(seq 1 30); do
|
|
224
|
+
vault status -format=json >/dev/null 2>&1 && break
|
|
225
|
+
[ "$i" = 30 ] && { cat "$RUNNER_TEMP/vault-dev.log"; exit 1; }
|
|
226
|
+
sleep 1
|
|
227
|
+
done
|
|
228
|
+
vault secrets enable transit
|
|
229
|
+
vault write transit/keys/floorvault-ci type=aes256-gcm96 derived=true
|
|
230
|
+
- name: Run the live-Vault contract test
|
|
231
|
+
env:
|
|
232
|
+
FLOORVAULT_LIVE_VAULT: "1"
|
|
233
|
+
VAULT_ADDR: "https://127.0.0.1:8200"
|
|
234
|
+
VAULT_CACERT: ${{ runner.temp }}/vault-tls/vault-ca.pem
|
|
235
|
+
VAULT_TOKEN: fv-ci-token
|
|
236
|
+
run: uv run pytest -q tests/test_vault_transit_live.py
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Drafted release/provenance controls (roadmap 0e). Two modes:
|
|
4
|
+
#
|
|
5
|
+
# workflow_dispatch - rehearsal: build, reproducible-build check, SBOM,
|
|
6
|
+
# artifact provenance attestation. Nothing is published.
|
|
7
|
+
# release: published - the same build, then PyPI publish via Trusted
|
|
8
|
+
# Publishing with PEP 740 (Sigstore) attestations.
|
|
9
|
+
#
|
|
10
|
+
# Publishing stays inert until the maintainer completes the owner-side setup in
|
|
11
|
+
# docs/RELEASING.md (PyPI project + Trusted Publisher entry for this workflow,
|
|
12
|
+
# the `pypi` GitHub environment). Until then a real release run fails loudly at
|
|
13
|
+
# the publish step rather than silently skipping it.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
release:
|
|
17
|
+
types: [published]
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
dist:
|
|
25
|
+
name: Build, verify, attest
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
id-token: write # artifact attestation signs via Sigstore OIDC
|
|
30
|
+
attestations: write
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
|
|
33
|
+
with:
|
|
34
|
+
fetch-depth: 0
|
|
35
|
+
|
|
36
|
+
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.13"
|
|
39
|
+
version: "0.12.5"
|
|
40
|
+
|
|
41
|
+
- name: Build release artifacts twice
|
|
42
|
+
# The comparison copy MUST be built outside the checkout: the sdist is
|
|
43
|
+
# VCS-aware, so a second in-repo out-dir appears mid-build and packages
|
|
44
|
+
# itself into the archive it is producing (dist-second/.gitignore was
|
|
45
|
+
# the first live demonstration). A mktemp dir can never self-include.
|
|
46
|
+
run: |
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
uv build --out-dir dist
|
|
49
|
+
SECOND="$(mktemp -d /tmp/floorvault-dist-second.XXXXXX)"
|
|
50
|
+
uv build --out-dir "$SECOND"
|
|
51
|
+
echo "SECOND=$SECOND" >> "$GITHUB_ENV"
|
|
52
|
+
|
|
53
|
+
- name: Reproducible-build check
|
|
54
|
+
# Same runner, same source, same toolchain: the digests must match
|
|
55
|
+
# byte-for-byte. Cross-PLATFORM identity is explicitly not claimed
|
|
56
|
+
# (SECURITY.md), but a single-environment build that cannot reproduce
|
|
57
|
+
# itself indicates the toolchain picked up nondeterminism.
|
|
58
|
+
run: |
|
|
59
|
+
set -euo pipefail
|
|
60
|
+
python - "$SECOND" <<'PY'
|
|
61
|
+
import hashlib
|
|
62
|
+
import pathlib
|
|
63
|
+
import sys
|
|
64
|
+
|
|
65
|
+
first = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in pathlib.Path("dist").iterdir()}
|
|
66
|
+
second = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in pathlib.Path(sys.argv[1]).iterdir()}
|
|
67
|
+
mismatched = sorted(n for n in set(first) | set(second) if first.get(n) != second.get(n))
|
|
68
|
+
if mismatched:
|
|
69
|
+
for name in mismatched:
|
|
70
|
+
print(f"mismatch {name}: {first.get(name)} != {second.get(name)}", file=sys.stderr)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
for name, digest in sorted(first.items()):
|
|
73
|
+
print(f"[REPRODUCIBLE] sha256 {digest} {name}")
|
|
74
|
+
PY
|
|
75
|
+
|
|
76
|
+
- name: Verify universal wheel shape
|
|
77
|
+
run: python scripts/verify_wheel.py dist
|
|
78
|
+
|
|
79
|
+
- name: Generate CycloneDX SBOM
|
|
80
|
+
# The SBOM describes what a consumer installs: a runtime-only sync, not
|
|
81
|
+
# the dev extras. cyclonedx-bom is version-pinned and >7 days old.
|
|
82
|
+
# It lives OUTSIDE dist/: the publish job uploads everything in dist/
|
|
83
|
+
# to PyPI, and a JSON file is not a valid distribution.
|
|
84
|
+
run: |
|
|
85
|
+
set -euo pipefail
|
|
86
|
+
uv sync
|
|
87
|
+
mkdir -p sbom
|
|
88
|
+
uvx --from "cyclonedx-bom==7.4.0" \
|
|
89
|
+
cyclonedx-py environment .venv -o sbom/floorvault-sbom.cdx.json --of JSON \
|
|
90
|
+
--mc-type library --pyproject pyproject.toml --output-reproducible
|
|
91
|
+
|
|
92
|
+
- name: Publish the built artifacts
|
|
93
|
+
# Two artifacts, not one: a multi-path upload keeps both directory
|
|
94
|
+
# names at the artifact root, so a single-artifact download would land
|
|
95
|
+
# sbom/ inside the publish job's dist/ - and the PyPI action uploads
|
|
96
|
+
# every file it finds there, which a JSON SBOM is not.
|
|
97
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
98
|
+
with:
|
|
99
|
+
name: floorvault-release-dist
|
|
100
|
+
path: dist/
|
|
101
|
+
if-no-files-found: error
|
|
102
|
+
|
|
103
|
+
- name: Publish the SBOM
|
|
104
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
|
105
|
+
with:
|
|
106
|
+
name: floorvault-release-sbom
|
|
107
|
+
path: sbom/
|
|
108
|
+
if-no-files-found: error
|
|
109
|
+
|
|
110
|
+
- name: Attest build provenance
|
|
111
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8
|
|
112
|
+
with:
|
|
113
|
+
subject-path: dist/
|
|
114
|
+
|
|
115
|
+
publish:
|
|
116
|
+
name: Publish to PyPI (Trusted Publishing)
|
|
117
|
+
needs: dist
|
|
118
|
+
if: github.event_name == 'release'
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
environment: pypi
|
|
121
|
+
permissions:
|
|
122
|
+
id-token: write # Trusted Publishing exchanges this OIDC token for PyPI
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
125
|
+
with:
|
|
126
|
+
name: floorvault-release-dist
|
|
127
|
+
path: dist
|
|
128
|
+
|
|
129
|
+
- name: Drop uv's out-dir marker
|
|
130
|
+
# `uv build --out-dir` writes a `.gitignore` into the directory; it is
|
|
131
|
+
# not a distribution and the PyPI action rejects unknown files.
|
|
132
|
+
run: rm -f dist/.gitignore
|
|
133
|
+
|
|
134
|
+
- name: Publish with PEP 740 attestations
|
|
135
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
|
136
|
+
with:
|
|
137
|
+
attestations: true
|
|
138
|
+
print-hash: true
|
|
139
|
+
verbose: true
|