ci: add angie/stable build matrix to ci-deep, remove stray AGENTS.md/CI_SETUP.md#79
Conversation
…CI_SETUP.md - ci-deep.yml's build-flavors job now builds+tests against nginx mainline, nginx stable, and angie monthly -- angie support was documented in README but never actually CI-tested. - ci-build.sh extended for a flavor arg, persists build tree under .build/ instead of a throwaway /tmp dir. - tools/bump-versions.sh + weekly bump.yml keep the stable/angie pins current (nginx mainline stays dynamically resolved at CI run time). - Removed AGENTS.md and .github/CI_SETUP.md: redundant with CLAUDE.md routing at the superrepo level and the README's own CI table.
📝 WalkthroughWalkthroughThe change adds persistent nginx/Angie build support, a CI matrix with targeted tests, and scheduled version-pin automation. It also updates changelog and ignore rules while removing the CI setup and repository guidance documents. ChangesCI and version automation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)Flavor matrix validationsequenceDiagram
participant GitHubActions as GitHub Actions
participant CiBuild as tools/ci-build.sh
participant TestNginx as Test::Nginx
GitHubActions->>CiBuild: build each flavor/version matrix entry
CiBuild-->>GitHubActions: return server and module artifacts
GitHubActions->>TestNginx: run targeted tests
TestNginx-->>GitHubActions: report results
Scheduled version updatessequenceDiagram
participant Workflow as Bump versions workflow
participant Script as tools/bump-versions.sh
participant Sources as Release sources
participant Repository as Repository files
Workflow->>Script: run weekly or manually
Script->>Sources: discover nginx and Angie releases
Script->>Repository: update version and checksum pins
Workflow->>Repository: commit and push changes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/bump.yml:
- Around line 30-31: Move the workflow-level contents: write permission into the
bump job definition, and add a brief comment documenting that the job needs
write access to commit and push to master. Leave other jobs or permissions
unchanged.
In @.github/workflows/ci-deep.yml:
- Around line 72-73: Update the “Checkout module” actions/checkout step to set
persist-credentials to false, preventing GitHub credentials from remaining in
the workspace while preserving the existing checkout behavior.
- Around line 83-117: Route matrix.flavor and matrix.version through step-level
env variables before using them in shell commands. Update the build step,
“Verify built modules,” and “Run t/” steps to reference those environment
variables instead of directly interpolating matrix values in run scripts,
preserving the existing paths, commands, and test behavior.
- Around line 61-64: Update the mainline matrix entry in the workflow to use an
empty version so ci-build.sh executes its mainline-resolution branch instead of
downloading a hardcoded nginx tarball. Ensure the resolved version is propagated
to subsequent steps that consume the matrix version, while leaving the stable
flavor entries unchanged.
In `@tools/ci-build.sh`:
- Around line 91-153: Update the tarball handling around the "$TARBALL" cache
check so cached archives are validated before reuse: verify nginx archives with
the existing PGP flow and Angie archives against ANGIE_SHA256 when a pinned
value exists. Download new archives to a temporary file, complete validation
successfully, then atomically move the file into "$TARBALL"; remove invalid or
partial files and preserve the existing failure behavior before unpacking.
- Around line 58-60: Replace the hardcoded ANGIE_SHA256 entry for version 1.11.5
with verification using Angie’s angie-1.11.5.tar.gz.asc signature and the
project signing key, or clearly document the authoritative checksum source if
one is available. Update the relevant tools/ci-build.sh download and
verification flow while preserving version-specific artifact validation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 34412f19-2fd7-4a76-b14a-bbf54dead469
📒 Files selected for processing (8)
.github/CI_SETUP.md.github/workflows/bump.yml.github/workflows/ci-deep.yml.gitignoreAGENTS.mdCHANGEStools/bump-versions.shtools/ci-build.sh
💤 Files with no reviewable changes (2)
- AGENTS.md
- .github/CI_SETUP.md
| permissions: | ||
| contents: write |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
Scope contents: write to the job, and document why.
Needed here since the job commits and pushes to master (Line 63), but zizmor flags it as workflow-level and undocumented. Moving it into the bump: job and adding a short comment keeps this workflow safe if a second job is ever added here without needing write access.
♻️ Suggested fix
-permissions:
- contents: write
-
jobs:
bump:
name: Bump nginx-stable/angie/nginx-tests pins
runs-on: [self-hosted, builder02, lxc]
timeout-minutes: 15
+ # Needs write access to commit and push the version-pin bump directly to master.
+ permissions:
+ contents: write
steps:🧰 Tools
🪛 zizmor (1.26.1)
[error] 31-31: overly broad permissions (excessive-permissions): contents: write is overly broad at the workflow level
(excessive-permissions)
[warning] 31-31: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/bump.yml around lines 30 - 31, Move the workflow-level
contents: write permission into the bump job definition, and add a brief comment
documenting that the job needs write access to commit and push to master. Leave
other jobs or permissions unchanged.
Source: Linters/SAST tools
| include: | ||
| - flavor: nginx | ||
| version: "1.31.2" | ||
| label: mainline |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
.github/workflows/ci-deep.yml:61-64 mainline should be resolved dynamically. version: "1.31.2" hardcodes the mainline leg, so this path never hits the ci-build.sh mainline-resolve branch and will eventually break when nginx.org rotates that tarball. Pass an empty version here and carry the resolved version forward for later steps.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci-deep.yml around lines 61 - 64, Update the mainline
matrix entry in the workflow to use an empty version so ci-build.sh executes its
mainline-resolution branch instead of downloading a hardcoded nginx tarball.
Ensure the resolved version is propagated to subsequent steps that consume the
matrix version, while leaving the stable flavor entries unchanged.
| - name: Checkout module | ||
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Add persist-credentials: false.
This job only builds/tests; it never commits or pushes. Persisting git credentials in the workspace is unnecessary exposure for a job that downloads and compiles third-party source (nginx/angie tarballs).
🔒 Fix
- name: Checkout module
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
+ with:
+ persist-credentials: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Checkout module | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| - name: Checkout module | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 72-73: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci-deep.yml around lines 72 - 73, Update the “Checkout
module” actions/checkout step to set persist-credentials to false, preventing
GitHub credentials from remaining in the workspace while preserving the existing
checkout behavior.
Source: Linters/SAST tools
| run: bash tools/ci-build.sh "${{ matrix.flavor }}" "${{ matrix.version }}" | ||
|
|
||
| - name: Verify built modules | ||
| run: | | ||
| build=".build/${{ matrix.flavor }}-${{ matrix.version }}/objs" | ||
| test -f "$build/${{ matrix.flavor }}" | ||
| test -f "$build/ngx_http_zstd_filter_module.so" | ||
| test -f "$build/ngx_http_zstd_static_module.so" | ||
| echo "TEST_NGINX_BINARY=$GITHUB_WORKSPACE/$build/${{ matrix.flavor }}" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Cache Perl modules | ||
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | ||
| with: | ||
| path: ~/perl5 | ||
| key: perl-modules-${{ runner.os }}-Test-Nginx-Socket | ||
|
|
||
| - name: Install Test::Nginx::Socket | ||
| run: | | ||
| cpanm -l ~/perl5 --notest Test::Nginx::Socket | ||
| echo "PERL5LIB=$HOME/perl5/lib/perl5:$PERL5LIB" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Run t/ (filter + static + conf-warn) | ||
| env: | ||
| # See build-test.yml's identical setting for why this must be 20, | ||
| # not the ~2s Test::Nginx::Socket default. | ||
| TEST_NGINX_TIMEOUT: "20" | ||
| run: | | ||
| export PERL5LIB="$HOME/perl5/lib/perl5:${PERL5LIB:-}" | ||
| export TEST_NGINX_SERVROOT="/tmp/nginx-servroot-${{ matrix.flavor }}-${{ matrix.version }}" | ||
| mkdir -p "$TEST_NGINX_SERVROOT" | ||
| cd "$GITHUB_WORKSPACE" | ||
| # t/01-static.t asserts on fixture mtimes (Last-Modified/If-Modified-Since); | ||
| # pin them to a fixed timestamp, same as build-test.yml's tests job. | ||
| touch -d @1541504307 t/suite/test t/suite/test.zst | ||
| prove -v t/00-filter.t t/01-static.t t/02-conf-warn.t |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | 💤 Low value
Template-injection warnings on matrix.flavor/matrix.version interpolation.
zizmor flags direct ${{ matrix.flavor }}/${{ matrix.version }} expansion into run: scripts (Lines 83, 87, 88, 91, 111). Actual risk here is low since these are static literals defined in this same workflow's matrix, not attacker-controlled input, but routing them through env: first is the standard mitigation and avoids the warning class entirely.
♻️ Example for the build step
- name: Build server and dynamic modules
- run: bash tools/ci-build.sh "${{ matrix.flavor }}" "${{ matrix.version }}"
+ env:
+ FLAVOR: ${{ matrix.flavor }}
+ FLAVOR_VERSION: ${{ matrix.version }}
+ run: bash tools/ci-build.sh "$FLAVOR" "$FLAVOR_VERSION"Apply the same pattern to the "Verify built modules" and "Run t/" steps.
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 83-83: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 83-83: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 87-87: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 87-87: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 88-88: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 91-91: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 111-111: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[warning] 111-111: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci-deep.yml around lines 83 - 117, Route matrix.flavor and
matrix.version through step-level env variables before using them in shell
commands. Update the build step, “Verify built modules,” and “Run t/” steps to
reference those environment variables instead of directly interpolating matrix
values in run scripts, preserving the existing paths, commands, and test
behavior.
Source: Linters/SAST tools
| declare -A ANGIE_SHA256=( | ||
| ["1.11.5"]="b5f297c6df2a74b9d0091a7cdd747fffd2d0e1d0be43632da61c1c7539db2043" | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
curl -fsSL https://download.angie.software/files/angie-1.11.5.tar.gz -o /tmp/angie.tar.gz
sha256sum /tmp/angie.tar.gzRepository: myguard-labs/nginx-zstd-module
Length of output: 215
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant file context
git ls-files tools/ci-build.sh
wc -l tools/ci-build.sh
cat -n tools/ci-build.sh | sed -n '45,75p'
# Recompute the published tarball SHA256 using Python
python3 - <<'PY'
import hashlib, urllib.request, sys
url = "https://download.angie.software/files/angie-1.11.5.tar.gz"
with urllib.request.urlopen(url, timeout=30) as r:
data = r.read()
print(hashlib.sha256(data).hexdigest())
PYRepository: myguard-labs/nginx-zstd-module
Length of output: 3991
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Try to resolve the published version metadata from the project release page as a cross-check.
python3 - <<'PY'
import urllib.request, re
url = "https://angie.software/en/download/"
html = urllib.request.urlopen(url, timeout=30).read().decode("utf-8", "replace")
for m in re.finditer(r'angie-1\.11\.5\.tar\.gz', html):
start = max(0, m.start()-200)
end = min(len(html), m.end()+200)
print(html[start:end])
break
else:
print("version not found")
PYRepository: myguard-labs/nginx-zstd-module
Length of output: 2762
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import hashlib, urllib.request
url = "https://download.angie.software/files/angie-1.11.5.tar.gz"
with urllib.request.urlopen(url, timeout=30) as r:
data = r.read()
print("sha256:", hashlib.sha256(data).hexdigest())
PYRepository: myguard-labs/nginx-zstd-module
Length of output: 2762
🌐 Web query:
Angie 1.11.5 tar.gz sha256 download.angie.software checksum
💡 Result:
The SHA256 checksum for angie-1.11.5.tar.gz is not explicitly provided as a public string on the official Angie documentation or file download index [1]. While the file angie-1.11.5.tar.gz and its associated PGP signature file angie-1.11.5.tar.gz.asc are available in the official download directory, the corresponding.sha256 file is not listed alongside them [1]. To verify the integrity of the downloaded source file, you should use the provided PGP signature file (angie-1.11.5.tar.gz.asc) and the official project GPG key [1][2]. You can find the public key at https://angie.software/keys/angie-signing.gpg.asc [2]. After importing the key, you can verify the signature using the following command: gpg --verify angie-1.11.5.tar.gz.asc angie-1.11.5.tar.gz
Citations:
- 1: https://download.angie.software/files/
- 2: https://en.angie.software/angie/docs/installation/oss_packages.md
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the angie-specific verification logic around the checksum map.
sed -n '70,120p' tools/ci-build.shRepository: myguard-labs/nginx-zstd-module
Length of output: 1889
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '70,120p' tools/ci-build.shRepository: myguard-labs/nginx-zstd-module
Length of output: 1889
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the Angie verification branch where the pinned checksum is consumed.
sed -n '120,190p' tools/ci-build.shRepository: myguard-labs/nginx-zstd-module
Length of output: 2473
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '120,190p' tools/ci-build.shRepository: myguard-labs/nginx-zstd-module
Length of output: 2473
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'ANGIE_SHA256|angie.*gpg|tar.gz\.asc|download\.angie\.software|angie-signing' tools/ci-build.shRepository: myguard-labs/nginx-zstd-module
Length of output: 399
Use Angie’s signature file instead of a hardcoded checksum. Angie doesn’t publish a SHA256 for angie-1.11.5.tar.gz; document the checksum source or verify angie-1.11.5.tar.gz.asc with the project key.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tools/ci-build.sh` around lines 58 - 60, Replace the hardcoded ANGIE_SHA256
entry for version 1.11.5 with verification using Angie’s angie-1.11.5.tar.gz.asc
signature and the project signing key, or clearly document the authoritative
checksum source if one is available. Update the relevant tools/ci-build.sh
download and verification flow while preserving version-specific artifact
validation.
| if [ "$NO_CACHE" = "1" ]; then | ||
| rm -rf "$SRCDIR" "$TARBALL" | ||
| fi | ||
|
|
||
| # Download nginx | ||
| echo "==========================================================================" | ||
| echo "Phase 1: Downloading nginx $NGINX_VERSION" | ||
| echo "Phase 1: Downloading $FLAVOR $VERSION" | ||
| echo "==========================================================================" | ||
| echo "" | ||
|
|
||
| tarball="nginx-${NGINX_VERSION}.tar.gz" | ||
|
|
||
| # Always fetch over HTTPS and verify the detached PGP signature against the | ||
| # nginx release-signing keys before unpacking. A plain HTTP download lets a | ||
| # network attacker swap the source that we then configure and compile. | ||
| if [ ! -f "$tarball" ]; then | ||
| wget -q "https://nginx.org/download/${tarball}" | ||
| echo "✓ Downloaded ${tarball}" | ||
| if [ ! -f "$TARBALL" ]; then | ||
| wget -q -O "$TARBALL" "$URL" | ||
| echo "✓ Downloaded ${DIR}.tar.gz" | ||
|
|
||
| if [ "$FLAVOR" = "nginx" ]; then | ||
| # Always fetch over HTTPS and verify the detached PGP signature | ||
| # against the nginx release-signing keys before unpacking. A plain | ||
| # HTTP download lets a network attacker swap the source that we then | ||
| # configure and compile. | ||
| wget -q "${URL}.asc" -O "${TARBALL}.asc" | ||
|
|
||
| gnupghome="$(mktemp -d)" | ||
| export GNUPGHOME="$gnupghome" | ||
| chmod 700 "$gnupghome" | ||
| for key in nginx_signing mdounin maxim sb thresh pluknet arut; do | ||
| wget -q "https://nginx.org/keys/${key}.key" -O - 2>/dev/null \ | ||
| | gpg --quiet --import 2>/dev/null || true | ||
| done | ||
|
|
||
| if gpg --quiet --verify "${TARBALL}.asc" "$TARBALL"; then | ||
| echo "✓ PGP signature verified for ${DIR}.tar.gz" | ||
| else | ||
| echo "✗ PGP signature verification FAILED for ${DIR}.tar.gz" >&2 | ||
| rm -rf "$gnupghome" "$TARBALL" "${TARBALL}.asc" | ||
| exit 1 | ||
| fi | ||
| rm -rf "$gnupghome" | ||
| unset GNUPGHOME | ||
| else | ||
| EXPECTED="${ANGIE_SHA256[$VERSION]:-}" | ||
| if [ -n "$EXPECTED" ]; then | ||
| ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')" | ||
| if [ "$ACTUAL" != "$EXPECTED" ]; then | ||
| echo "✗ sha256 mismatch for ${DIR}.tar.gz" >&2 | ||
| echo " expected: $EXPECTED" >&2 | ||
| echo " actual: $ACTUAL" >&2 | ||
| rm -f "$TARBALL" | ||
| exit 1 | ||
| fi | ||
| echo "✓ sha256 verified for ${DIR}.tar.gz" | ||
| else | ||
| echo "WARNING: no pinned sha256 for angie $VERSION -- add one to" \ | ||
| "ANGIE_SHA256 in tools/ci-build.sh (downloaded tarball is" \ | ||
| "UNVERIFIED)" >&2 | ||
| fi | ||
| fi | ||
| else | ||
| echo "✓ Using cached ${tarball}" | ||
| echo "✓ Using cached ${DIR}.tar.gz" | ||
| fi | ||
|
|
||
| # Detached signature. | ||
| wget -q "https://nginx.org/download/${tarball}.asc" -O "${tarball}.asc" | ||
|
|
||
| # nginx release-signing public keys (served over HTTPS by nginx.org). Import | ||
| # whichever exist so verification keeps working as the active signer rotates. | ||
| gnupghome="$(mktemp -d)" | ||
| export GNUPGHOME="$gnupghome" | ||
| chmod 700 "$gnupghome" | ||
| for key in nginx_signing mdounin maxim sb thresh pluknet arut; do | ||
| wget -q "https://nginx.org/keys/${key}.key" -O - 2>/dev/null \ | ||
| | gpg --quiet --import 2>/dev/null || true | ||
| done | ||
|
|
||
| if gpg --quiet --verify "${tarball}.asc" "$tarball"; then | ||
| echo "✓ PGP signature verified for ${tarball}" | ||
| else | ||
| echo "✗ PGP signature verification FAILED for ${tarball}" >&2 | ||
| rm -rf "$gnupghome" | ||
| exit 1 | ||
| if [ ! -d "$SRCDIR" ]; then | ||
| tar -xzf "$TARBALL" -C "$ROOT" | ||
| mv "$ROOT/$DIR" "$SRCDIR" | ||
| fi |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant script region with line numbers.
cat -n tools/ci-build.sh | sed -n '1,240p'
# Probe the control flow: confirm verification is only inside the cache-miss branch.
python3 - <<'PY'
from pathlib import Path
text = Path("tools/ci-build.sh").read_text()
for needle in [
'if [ ! -f "$TARBALL" ]; then',
'wget -q -O "$TARBALL" "$URL"',
'if [ "$FLAVOR" = "nginx" ]; then',
'EXPECTED="${ANGIE_SHA256[$VERSION]:-}"',
'echo "✓ Using cached ${DIR}.tar.gz"',
]:
print(f"{needle!r}:", text.find(needle))
PYRepository: myguard-labs/nginx-zstd-module
Length of output: 9270
Check cached tarballs before unpacking — tools/ci-build.sh:100-152 trusts any existing $TARBALL, so a poisoned or partially written cache entry skips both the nginx PGP check and the angie sha256 check. Download to a temp file, and validate cached archives before reuse.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tools/ci-build.sh` around lines 91 - 153, Update the tarball handling around
the "$TARBALL" cache check so cached archives are validated before reuse: verify
nginx archives with the existing PGP flow and Angie archives against
ANGIE_SHA256 when a pinned value exists. Download new archives to a temporary
file, complete validation successfully, then atomically move the file into
"$TARBALL"; remove invalid or partial files and preserve the existing failure
behavior before unpacking.
Summary
Test plan
Summary by CodeRabbit
New Features
Documentation
Chores