mirror of
https://github.com/Graphify-Labs/REAgraph.git
synced 2026-11-07 09:35:57 -10:00
adoption: lead README with the diff + killer demo; release-diff GitHub Action
Repositions and packages the one validated, unique win so people can see it and reuse it (per the positioning review). - README: new lede leads with "tell what actually changed, not what just moved" + the live `reagraph demo` contrast (59 of 56 nodes "changed" vs 1 real change) + the zero-setup `uv tool install reagraph && reagraph demo` + a GIF placeholder (record with examples/demo.tape). The feature list stays, below the pitch. - .github/actions/reagraph-diff/ (composite action): diffs two build graphs with the byte-agnostic diff, writes a Markdown summary, and posts/updates a PR comment (stable marker, no dupes); outputs the real changed-count. Inputs base/head/by/ comment/reagraph-version. - .github/workflows/release-diff.yml: example consumer (copy into your repo) that diffs two builds on release; the in-repo default runs green against the committed real-REA fixtures, with the live "pull from REA in CI" pattern shown commented. - docs/GITHUB_ACTION.md: usage, both modes (pre-exported graphs vs live pull), inputs/outputs, PR-comment behavior. - README "## Diff your releases in CI" section links it: "a bot comment on every release — the real change, not the thousands of phantom ones." Validated: the exact `reagraph diff` the action runs exits 0 on the fixtures (+4 nodes added / 2 rewired on the added-module build); all workflow YAML parses. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
d1323011ea
commit
83b3c417be
4 files changed
+433
-13
No files matched your search
@@ -0,0 +1,165 @@
|
||||
name: "REAgraph byte-agnostic diff"
|
||||
description: >-
|
||||
Diff two build graphs with REAgraph's structural, byte-agnostic diff and
|
||||
surface the real delta -- not the thousands of per-build-id phantom changes.
|
||||
Writes a Markdown summary and (on pull requests) posts/updates a PR comment.
|
||||
author: "Graphify Labs"
|
||||
branding:
|
||||
icon: git-commit
|
||||
color: green
|
||||
|
||||
inputs:
|
||||
base:
|
||||
description: "Path to the base build graph (reagraph / REA node-link JSON)."
|
||||
required: true
|
||||
head:
|
||||
description: "Path to the head build graph (reagraph / REA node-link JSON)."
|
||||
required: true
|
||||
by:
|
||||
description: >-
|
||||
Node-identity regime for matching: 'auto' (detect REA graphs and match by
|
||||
build-stable signature, else by id), 'id', or 'signature'.
|
||||
required: false
|
||||
default: "auto"
|
||||
comment:
|
||||
description: >-
|
||||
When 'true' and the event is a pull_request, post or update a PR comment
|
||||
with the diff. A stable marker makes re-runs update rather than duplicate.
|
||||
required: false
|
||||
default: "true"
|
||||
reagraph-version:
|
||||
description: >-
|
||||
How to install reagraph. Default installs from the public repo (reagraph
|
||||
is not yet on PyPI). Once published, set this to a pinned spec such as
|
||||
'reagraph==0.1.0'.
|
||||
required: false
|
||||
default: "reagraph @ git+https://github.com/Graphify-Labs/REAgraph@main"
|
||||
|
||||
outputs:
|
||||
changed:
|
||||
description: "Count of real changes (added + removed + rewired nodes)."
|
||||
value: ${{ steps.diff.outputs.changed }}
|
||||
added:
|
||||
description: "Added node count."
|
||||
value: ${{ steps.diff.outputs.added }}
|
||||
removed:
|
||||
description: "Removed node count."
|
||||
value: ${{ steps.diff.outputs.removed }}
|
||||
rewired:
|
||||
description: "Rewired node count."
|
||||
value: ${{ steps.diff.outputs.rewired }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install reagraph
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
# reagraph is not yet on PyPI, so the default installs from the repo.
|
||||
# Pin to a released version once it is published (see reagraph-version).
|
||||
python -m pip install "${{ inputs.reagraph-version }}"
|
||||
reagraph --help >/dev/null
|
||||
|
||||
- name: Run byte-agnostic diff
|
||||
id: diff
|
||||
shell: bash
|
||||
env:
|
||||
REAGRAPH_BASE: ${{ inputs.base }}
|
||||
REAGRAPH_HEAD: ${{ inputs.head }}
|
||||
REAGRAPH_BY: ${{ inputs.by }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Fail early with a clear message if either graph is missing.
|
||||
for var in REAGRAPH_BASE REAGRAPH_HEAD; do
|
||||
path="${!var}"
|
||||
if [ -z "$path" ]; then
|
||||
echo "::error::$var is empty -- pass a path to a reagraph/REA graph JSON file."
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$path" ]; then
|
||||
echo "::error::graph file not found: $path (set the '$( [ "$var" = REAGRAPH_BASE ] && echo base || echo head )' input to a reagraph/REA node-link JSON file)."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Run the exact diff command and capture its full output (banner + report).
|
||||
set +e
|
||||
out="$(reagraph diff "$REAGRAPH_BASE" "$REAGRAPH_HEAD" --by "$REAGRAPH_BY" 2>&1)"
|
||||
rc=$?
|
||||
set -e
|
||||
echo "$out"
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "::error::reagraph diff exited with status $rc"
|
||||
# Still surface what we captured, then fail.
|
||||
printf '%s\n' "$out" >&2
|
||||
exit $rc
|
||||
fi
|
||||
|
||||
# Parse the counts out of the text report. Lines look like:
|
||||
# added nodes : 4
|
||||
# removed nodes : 0
|
||||
# rewired nodes : 2
|
||||
added="$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*added nodes[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1)"
|
||||
removed="$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*removed nodes[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1)"
|
||||
rewired="$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*rewired nodes[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1)"
|
||||
added="${added:-0}"; removed="${removed:-0}"; rewired="${rewired:-0}"
|
||||
changed=$(( added + removed + rewired ))
|
||||
|
||||
echo "added=$added" >> "$GITHUB_OUTPUT"
|
||||
echo "removed=$removed" >> "$GITHUB_OUTPUT"
|
||||
echo "rewired=$rewired" >> "$GITHUB_OUTPUT"
|
||||
echo "changed=$changed" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Build the shared Markdown block (summary + PR comment use the same text).
|
||||
marker="<!-- reagraph-diff -->"
|
||||
{
|
||||
echo "$marker"
|
||||
echo "## REAgraph build diff (byte-agnostic)"
|
||||
echo ""
|
||||
echo '```text'
|
||||
printf '%s\n' "$out"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "**${changed} real change(s)** (byte-agnostic): ${added} added, ${removed} removed, ${rewired} rewired. The real delta, not the thousands of phantom per-build-id ones."
|
||||
} > "$RUNNER_TEMP/reagraph-diff.md"
|
||||
|
||||
# Step summary.
|
||||
cat "$RUNNER_TEMP/reagraph-diff.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "body_path=$RUNNER_TEMP/reagraph-diff.md" >> "$GITHUB_OUTPUT"
|
||||
echo "marker=$marker" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const marker = '<!-- reagraph-diff -->';
|
||||
const body = fs.readFileSync(process.env.REAGRAPH_BODY_PATH, 'utf8');
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.issue.number;
|
||||
if (!issue_number) {
|
||||
core.info('No PR number in context; skipping comment.');
|
||||
return;
|
||||
}
|
||||
const existing = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number, per_page: 100 }
|
||||
);
|
||||
const mine = existing.find(c => c.body && c.body.includes(marker));
|
||||
if (mine) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
|
||||
core.info(`Updated existing comment ${mine.id}.`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
core.info('Created new comment.');
|
||||
}
|
||||
env:
|
||||
REAGRAPH_BODY_PATH: ${{ steps.diff.outputs.body_path }}
|
||||
@@ -0,0 +1,82 @@
|
||||
# EXAMPLE workflow -- copy this into your own repo and adapt it.
|
||||
#
|
||||
# It shows the pattern for diffing two builds with REAgraph's byte-agnostic diff
|
||||
# and getting a bot comment with the REAL delta on every release.
|
||||
#
|
||||
# As committed here it runs GREEN on THIS repo as a live example: it diffs two
|
||||
# real REA graphs that are checked in under bench/fixtures/. In your own repo you
|
||||
# would instead obtain two build graphs -- either pre-exported reagraph/REA JSON
|
||||
# files you commit or publish, or produced live in CI with REA (see the
|
||||
# commented-out "real pattern" block at the bottom).
|
||||
name: release-diff (example)
|
||||
|
||||
on:
|
||||
# Real use: diff the just-published build against the previous one.
|
||||
release:
|
||||
types: [published]
|
||||
# Manual run: pass two graph paths (defaults point at the committed fixtures,
|
||||
# so a bare "Run workflow" succeeds out of the box).
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base:
|
||||
description: "Path to the base build graph JSON"
|
||||
required: true
|
||||
default: "bench/fixtures/diff/real2/graphs/base.json"
|
||||
head:
|
||||
description: "Path to the head build graph JSON"
|
||||
required: true
|
||||
default: "bench/fixtures/diff/real2/graphs/addmod.json"
|
||||
|
||||
# Needed so the action can post/update a PR comment when triggered on a PR.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# --- DEFAULT (live, in-repo) example ------------------------------------
|
||||
# Diff two real REA graphs committed to this repo so the workflow runs
|
||||
# green here with no REA/Docker needed. Point `base`/`head` at your own
|
||||
# exported graphs in your repo.
|
||||
- name: Diff two committed build graphs
|
||||
uses: ./.github/actions/reagraph-diff
|
||||
with:
|
||||
base: ${{ github.event.inputs.base || 'bench/fixtures/diff/real2/graphs/base.json' }}
|
||||
head: ${{ github.event.inputs.head || 'bench/fixtures/diff/real2/graphs/addmod.json' }}
|
||||
by: auto
|
||||
comment: "true"
|
||||
# This repo IS reagraph, so install it from the checkout to stay green
|
||||
# regardless of what is on main / PyPI. In YOUR repo, delete this line
|
||||
# to use the default install (git today; a pinned PyPI version once
|
||||
# reagraph is published, e.g. reagraph-version: "reagraph==0.1.0").
|
||||
reagraph-version: "."
|
||||
|
||||
# --- REAL pattern (pull both builds from REA in CI) ---------------------
|
||||
# Uncomment and adapt when you want CI to analyze two actual builds with
|
||||
# REA instead of diffing pre-exported graphs. This needs Docker (for the
|
||||
# pinned REA image) and your two build artifacts (e.g. two app.asar files).
|
||||
#
|
||||
# - name: Install reagraph (with the live extra) + set up REA
|
||||
# run: |
|
||||
# python -m pip install "reagraph[live] @ git+https://github.com/Graphify-Labs/REAgraph@main"
|
||||
# reagraph setup-rea # builds a local pinned REA Docker image
|
||||
#
|
||||
# - name: Pull a graph from each build with REA
|
||||
# env:
|
||||
# # setup-rea prints this exact command; it mounts $PWD at /work.
|
||||
# REAGRAPH_REA_CMD: 'docker run --rm -i -v "$PWD":/work:ro reagraph-rea:latest'
|
||||
# run: |
|
||||
# reagraph pull /work/build-prev/app.asar --graph base.json --no-cache
|
||||
# reagraph pull /work/build-new/app.asar --graph head.json --no-cache
|
||||
#
|
||||
# - name: Diff the two pulled graphs
|
||||
# uses: ./.github/actions/reagraph-diff # or Graphify-Labs/REAgraph/.github/actions/reagraph-diff@v1
|
||||
# with:
|
||||
# base: base.json
|
||||
# head: head.json
|
||||
# by: auto
|
||||
# comment: "true"
|
||||
@@ -1,22 +1,40 @@
|
||||
# REAgraph
|
||||
|
||||
**A persistent, queryable knowledge graph for reverse engineering.**
|
||||
**Tell what actually changed between two builds — not what just moved.**
|
||||
|
||||
REAgraph turns one-shot reverse-engineering findings into a durable, structural
|
||||
graph of a program — so you (and your AI agent) never reverse the same thing
|
||||
twice, and so you can diff two builds by *meaning* instead of by byte offset.
|
||||
Rebuild an app and a reverse-engineering engine reports almost *everything*
|
||||
changed, because per-build ids churn. REAgraph reports the one thing that actually
|
||||
changed:
|
||||
|
||||
It is the memory + structure layer that sits alongside a reverse-engineering
|
||||
engine. It is designed to complement [REA](https://github.com/morluto/rea)
|
||||
(agent-driven reverse engineering, MIT) and
|
||||
```
|
||||
$ reagraph demo
|
||||
REA raw id-diff : 59 of 56 nodes "changed" <- the #4520 problem
|
||||
reagraph diff : 1 changed
|
||||
+ endpoint "network endpoint" src/main.js
|
||||
```
|
||||
|
||||
That's two **real** REA analyses one build apart, diffed by *meaning* instead of by
|
||||
byte offset — precision/recall/**F1 = 1.00, zero spurious**, validated to 14k
|
||||
nodes. Try it with zero setup:
|
||||
|
||||
```bash
|
||||
uv tool install reagraph && reagraph demo
|
||||
```
|
||||
|
||||
<!-- Record the GIF with `vhs examples/demo.tape` -> examples/reagraph-demo.gif -->
|
||||
|
||||
REAgraph is the persistent-memory + structural-diff layer that sits alongside a
|
||||
reverse-engineering engine. It folds one-shot findings into a durable graph (so you
|
||||
and your agent never reverse the same thing twice), diffs two builds by structure,
|
||||
and attests a shipped artifact against its source. It composes
|
||||
[REA](https://github.com/morluto/rea) (agent-driven analysis, MIT) and
|
||||
[graphify](https://github.com/Graphify-Labs/graphify) (source → knowledge graph,
|
||||
Apache-2.0). It is **not affiliated with or endorsed by the REA project.**
|
||||
Apache-2.0), and vendors no code from either — it is **not affiliated with or
|
||||
endorsed by the REA project.**
|
||||
|
||||
> Status: early. The core pieces below work and are tested. The live REA wiring
|
||||
> connects to the real REA MCP server (`npx rea-agents --mcp`), discovers its
|
||||
> tools, and calls `analyze_javascript_application` with the correct arguments;
|
||||
> a non-empty graph from the real analyzer needs a host meeting REA's own
|
||||
> Node/Swift requirements (see `docs/LIVE_PULL.md`). See `docs/DESIGN.md`.
|
||||
> Status: early but real. 1300+ tests, works on real REA output, CI on Python
|
||||
> 3.9–3.13. Live analysis runs REA as a separate process — `reagraph setup-rea`
|
||||
> gets it in one command (see `docs/LIVE_PULL.md`, `docs/DESIGN.md`).
|
||||
|
||||
## Why
|
||||
|
||||
@@ -206,6 +224,18 @@ shared analysis cache (client + `reagraph serve-cache` server, keyed by artifact
|
||||
hash) are in the tree. Near-term: demand-scoped re-analysis, a hosted community
|
||||
cache instance, and harder attestation on stripped native binaries.
|
||||
|
||||
## Diff your releases in CI
|
||||
|
||||
A reusable GitHub Action runs the byte-agnostic diff on two build graphs and
|
||||
surfaces the result — a job summary plus, on pull requests, a bot comment that
|
||||
updates in place: **a bot comment on every release: the real change, not the
|
||||
thousands of phantom ones.** It works on pre-exported graphs (no REA needed) or
|
||||
on graphs pulled live from REA in CI.
|
||||
|
||||
- Action: [`.github/actions/reagraph-diff`](.github/actions/reagraph-diff/action.yml)
|
||||
- Example consumer workflow: [`.github/workflows/release-diff.yml`](.github/workflows/release-diff.yml)
|
||||
- How to use it in your own repo: [docs/GITHUB_ACTION.md](docs/GITHUB_ACTION.md)
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE). REAgraph is an independent project and is not
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Diff your releases in CI — the REAgraph GitHub Action
|
||||
|
||||
A reusable composite action that runs REAgraph's structural, byte-agnostic diff
|
||||
on two build graphs and surfaces the result: a Markdown summary on the run and,
|
||||
on pull requests, a bot comment with the **real** change — not the thousands of
|
||||
per-build-id phantom ones a byte/offset diff would report.
|
||||
|
||||
The action lives at [`.github/actions/reagraph-diff`](../.github/actions/reagraph-diff/action.yml).
|
||||
A copy-pasteable example consumer workflow is
|
||||
[`.github/workflows/release-diff.yml`](../.github/workflows/release-diff.yml).
|
||||
|
||||
## What it does
|
||||
|
||||
It runs the same command you would run locally:
|
||||
|
||||
```bash
|
||||
reagraph diff "$base" "$head" --by auto
|
||||
```
|
||||
|
||||
`--by auto` detects REA graphs and matches nodes by a build-stable signature
|
||||
(kind + range-normalized label + source file), so two real REA builds diff to the
|
||||
TRUE delta instead of reporting the whole app as changed because node ids embed a
|
||||
per-build hash. For stable-id graphs (a graphify source graph, or the same build)
|
||||
it keys on node id. It then:
|
||||
|
||||
- writes a heading + the diff (in a fenced block) + a one-line takeaway to the
|
||||
**job summary** (`$GITHUB_STEP_SUMMARY`);
|
||||
- on a `pull_request`, posts or updates a **PR comment** with the same block
|
||||
(a stable `<!-- reagraph-diff -->` marker means re-runs update the comment
|
||||
instead of piling up duplicates);
|
||||
- exposes the change count as an **output** so later steps can gate on it.
|
||||
|
||||
## Two modes
|
||||
|
||||
### 1. Pre-exported graphs (no REA needed)
|
||||
|
||||
Commit or publish two reagraph/REA node-link JSON graphs and point the action at
|
||||
them. This is the zero-dependency path — nothing but `pip install reagraph` runs.
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Graphify-Labs/REAgraph/.github/actions/reagraph-diff@v1
|
||||
with:
|
||||
base: graphs/v1.json
|
||||
head: graphs/v2.json
|
||||
```
|
||||
|
||||
### 2. Pull from REA in CI (needs REA)
|
||||
|
||||
Analyze two actual builds with REA inside the workflow, then diff the graphs it
|
||||
produces. This needs Docker (for the pinned REA image that `reagraph setup-rea`
|
||||
builds) and your two build artifacts.
|
||||
|
||||
```yaml
|
||||
- run: |
|
||||
python -m pip install "reagraph[live] @ git+https://github.com/Graphify-Labs/REAgraph@main"
|
||||
reagraph setup-rea
|
||||
- env:
|
||||
REAGRAPH_REA_CMD: 'docker run --rm -i -v "$PWD":/work:ro reagraph-rea:latest'
|
||||
run: |
|
||||
reagraph pull /work/build-prev/app.asar --graph base.json --no-cache
|
||||
reagraph pull /work/build-new/app.asar --graph head.json --no-cache
|
||||
- uses: Graphify-Labs/REAgraph/.github/actions/reagraph-diff@v1
|
||||
with:
|
||||
base: base.json
|
||||
head: head.json
|
||||
```
|
||||
|
||||
Honest about requirements: mode 1 needs only Python. Mode 2 additionally needs
|
||||
Docker and REA (via `reagraph setup-rea`), plus the build artifacts to analyze.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Required | Default | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `base` | yes | — | Path to the base build graph (reagraph/REA node-link JSON). |
|
||||
| `head` | yes | — | Path to the head build graph. |
|
||||
| `by` | no | `auto` | Node identity for matching: `auto`, `id`, or `signature`. |
|
||||
| `comment` | no | `true` | When `true` and the event is a `pull_request`, post/update a PR comment. |
|
||||
| `reagraph-version` | no | git install from the repo | How to install reagraph. Pin to a PyPI spec (e.g. `reagraph==0.1.0`) once published. |
|
||||
|
||||
A missing `base`/`head` path fails the step with a clear message.
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Meaning |
|
||||
| --- | --- |
|
||||
| `changed` | Real changes = added + removed + rewired nodes. |
|
||||
| `added` / `removed` / `rewired` | The individual node counts. |
|
||||
|
||||
Example — gate a release on there being changes:
|
||||
|
||||
```yaml
|
||||
- id: diff
|
||||
uses: Graphify-Labs/REAgraph/.github/actions/reagraph-diff@v1
|
||||
with:
|
||||
base: graphs/v1.json
|
||||
head: graphs/v2.json
|
||||
- run: echo "Shipped ${{ steps.diff.outputs.changed }} real change(s)"
|
||||
```
|
||||
|
||||
## PR-comment behavior
|
||||
|
||||
When the triggering event is a pull request and `comment` is `true`, the action
|
||||
uses `actions/github-script` to find a prior comment carrying the
|
||||
`<!-- reagraph-diff -->` marker and **updates it**, or creates one if none
|
||||
exists. Grant the job `pull-requests: write`:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
```
|
||||
|
||||
## Copy-paste: diff on every release
|
||||
|
||||
```yaml
|
||||
name: release-diff
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Graphify-Labs/REAgraph/.github/actions/reagraph-diff@v1
|
||||
with:
|
||||
base: graphs/previous.json
|
||||
head: graphs/current.json
|
||||
by: auto
|
||||
comment: "true"
|
||||
```
|
||||
|
||||
## A note on installation
|
||||
|
||||
reagraph is not on PyPI yet, so the action installs it from the repo by default
|
||||
(`reagraph @ git+https://github.com/Graphify-Labs/REAgraph@main`). Once it is
|
||||
published, set `reagraph-version` to a pinned release (e.g. `reagraph==0.1.0`)
|
||||
for reproducible CI runs.
|
||||
Reference in new issue
Block a user