Skip to content

feat: respect $SOURCE_DATE_EPOCH when generating reproducible output - #1084

Open
Yusuf-Gadelrab wants to merge 1 commit into
CycloneDX:mainfrom
Yusuf-Gadelrab:feat/source-date-epoch
Open

feat: respect $SOURCE_DATE_EPOCH when generating reproducible output#1084
Yusuf-Gadelrab wants to merge 1 commit into
CycloneDX:mainfrom
Yusuf-Gadelrab:feat/source-date-epoch

Conversation

@Yusuf-Gadelrab

Copy link
Copy Markdown

Description

Makes --output-reproducible honor $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_EPOCH metadata.timestamp
no anything current time (unchanged)
yes unset / empty omitted (unchanged)
yes invalid omitted + a WARNING log line (unchanged output)
yes valid that instant, in UTC

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(), returns Optional[datetime]; _make_output() assigns its result where it previously hardcoded None. serial_number stays None, 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() and datetime.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, and run_cli() calls the CLI in-process, so a developer or distro build with SOURCE_DATE_EPOCH already exported would suddenly get timestamps in the snapshots. I confirmed it: with the variable set and no guard, tests.integration.test_cli_requirements fails 240 of 283 tests. So run_cli() now pops that one variable inside a patch.dict(environ) and leaves the rest of the environment alone.

I first tried fixing this with SOURCE_DATE_EPOCH= in the tox setenv block and backed it out: an empty value makes setuptools fail with ValueError: invalid literal for int() with base 10: '' while building the environment testbeds. Worth knowing before anyone reaches for that.

Docs: added the new line to the --output-reproducible help text and to all four rendered help blocks in docs/usage.rst.

Tests. 13 new cases in tests/unit/test_cli.py, all going through the real Command and 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.

tests.unit                    19 tests   OK
tests.functional              53 tests   OK
tests.integration (cli, requirements, poetry, pipenv, pep621)
                            1363 tests   OK

Also verified with SOURCE_DATE_EPOCH=1700000000 exported in the shell: still 1363 OK.

flake8, isort, mypy, autopep8, pyupgrade and bandit are all clean on the changed files. (cyclonedx_py/_internal/poetry.py reports two B042 from flake8-bugbear, but those reproduce on a clean checkout of main and are untouched here.)

tests.integration.test_cli_environment I could not run locally: its testbed init fails on my machine for via-pdm, identically on an unmodified main, 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

  • My contribution does not include any AI-generated content
  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: Claude Code
    • LLMs and versions: Claude Opus 5
    • Prompts: Asked 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

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>
@Yusuf-Gadelrab
Yusuf-Gadelrab requested a review from a team as a code owner July 31, 2026 17:02
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 CycloneDX Python SBOM Tool | 🛠️ Build #33856589 | 📁 Comparing 1185bf4 against latest (5172a90)

  🔍 Preview build  

1 file changed
± usage.html

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 5 complexity · 0 duplication

Metric Results
Complexity 5 (≤ 20 complexity)
Duplication 0

View in Codacy

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant