feat: respect $SOURCE_DATE_EPOCH when generating reproducible output - #1084
feat: respect $SOURCE_DATE_EPOCH when generating reproducible output#1084Yusuf-Gadelrab wants to merge 1 commit into
$SOURCE_DATE_EPOCH when generating reproducible output#1084Conversation
When `--output-reproducible` is given and the environment variable `SOURCE_DATE_EPOCH` holds a valid non-negative UNIX timestamp, that value is used as the SBOM's `metadata.timestamp` instead of omitting it. Without the flag, or with an unset/invalid value, the existing behavior is unchanged. See <https://reproducible-builds.org/docs/source-date-epoch/>. Resolves CycloneDX#1036 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Yusuf Gadelrab <yusuf.gadelrab06@gmail.com>
Documentation build overview
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | ✅ 5 (≤ 20 complexity) |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull request overview
Updates the CLI’s reproducible-output mode to optionally set metadata.timestamp from $SOURCE_DATE_EPOCH, enabling deterministic “real” timestamps for SBOMs when desired, while keeping existing behavior unchanged for non-reproducible output.
Changes:
- Add
Command._source_date_epoch()to parse$SOURCE_DATE_EPOCH(only when--output-reproducibleis enabled) and apply it tobom.metadata.timestamp. - Harden test execution against ambient
SOURCE_DATE_EPOCHby clearing it insidetests.integration.run_cli()so snapshot tests remain stable. - Add unit tests covering valid/invalid/unset/whitespace/edge-case
SOURCE_DATE_EPOCHvalues, and document the behavior indocs/usage.rst.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
cyclonedx_py/_internal/cli.py |
Adds $SOURCE_DATE_EPOCH support behind --output-reproducible and wires it into SBOM timestamp generation. |
tests/unit/test_cli.py |
Adds unit coverage for parsing and applying $SOURCE_DATE_EPOCH (and ensuring it’s ignored without reproducible mode). |
tests/integration/__init__.py |
Makes snapshot-based integration tests deterministic by removing SOURCE_DATE_EPOCH from the in-process CLI environment. |
docs/usage.rst |
Updates rendered CLI help text to describe the new $SOURCE_DATE_EPOCH behavior under --output-reproducible. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Description
Makes
--output-reproduciblehonor$SOURCE_DATE_EPOCH, so an SBOM can carry a real, deterministic timestamp instead of no timestamp at all.Behavior follows exactly what @jkowalleck laid out in the issue thread:
--output-reproducible$SOURCE_DATE_EPOCHmetadata.timestampWARNINGlog line (unchanged output)The env var is only ever read behind the flag, so nothing changes for anyone who does not opt in.
Approach. One new method,
Command._source_date_epoch(), returnsOptional[datetime];_make_output()assigns its result where it previously hardcodedNone.serial_numberstaysNone, since a serial number is random-based and cannot be derived from a timestamp."Valid" means a non-negative integer of seconds, parsed with
int()anddatetime.fromtimestamp(..., tz=timezone.utc). Surrounding whitespace is stripped. Empty or whitespace-only is treated as unset rather than invalid, which is what build systems that conditionally export the variable actually produce. Anything else logs a warning and falls back to omitting the timestamp, so a typo in CI never silently poisons an SBOM.One thing worth flagging. Adding this made the test suite sensitive to the environment it runs in, which is a little ironic for a reproducibility feature. Every snapshot test passes
--output-reproducible, andrun_cli()calls the CLI in-process, so a developer or distro build withSOURCE_DATE_EPOCHalready exported would suddenly get timestamps in the snapshots. I confirmed it: with the variable set and no guard,tests.integration.test_cli_requirementsfails 240 of 283 tests. Sorun_cli()now pops that one variable inside apatch.dict(environ)and leaves the rest of the environment alone.I first tried fixing this with
SOURCE_DATE_EPOCH=in the toxsetenvblock and backed it out: an empty value makes setuptools fail withValueError: invalid literal for int() with base 10: ''while building theenvironmenttestbeds. Worth knowing before anyone reaches for that.Docs: added the new line to the
--output-reproduciblehelp text and to all four rendered help blocks indocs/usage.rst.Tests. 13 new cases in
tests/unit/test_cli.py, all going through the realCommandand asserting on parsed JSON output: unset, empty, whitespace-only, epoch zero, a valid timestamp, a value padded with whitespace, a non-number, a float, a negative, an out-of-range integer, plus one that the env var is ignored without the flag and one that an invalid value logs the warning.Also verified with
SOURCE_DATE_EPOCH=1700000000exported in the shell: still 1363 OK.flake8, isort, mypy, autopep8, pyupgrade and bandit are all clean on the changed files. (
cyclonedx_py/_internal/poetry.pyreports twoB042from flake8-bugbear, but those reproduce on a clean checkout ofmainand are untouched here.)tests.integration.test_cli_environmentI could not run locally: its testbed init fails on my machine forvia-pdm, identically on an unmodifiedmain, so it is a local toolchain gap rather than anything from this change. Leaving that one to CI.Resolves or fixes issue: #1036
AI Tool Disclosure
Claude CodeClaude Opus 5Asked it to find an unclaimed "help wanted" issue in this repo and implement it. For #1036 I directed it to follow the maintainer's stated expectations in the issue thread exactly (env var only honored behind --output-reproducible, current behavior otherwise), to add tests covering valid/invalid/unset inputs, and to run the full suite plus the linters and type checker. I reviewed the diff and the test results before opening this.Affirmation