Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4e3e178
ci: sibling checkouts follow the pull request's base branch, keeping …
jdogresorg Sep 17, 2026
12de40b
test: split decoder halt diagnostics
jdogresorg Sep 17, 2026
11ab78e
fix(reorg): widen Litecoin testnet rollback window
jdogresorg Sep 17, 2026
0ec2e5b
merge: level develop with master for the v0.20.1 patch train
jdogresorg Sep 19, 2026
9934191
test: parameterize fixture host ports
jdogresorg Sep 18, 2026
6fde4da
refactor(decoder): move migrate.js into src/db/
jdogresorg Sep 15, 2026
ae5bae2
docs(decoder): preserve rewritten comment detail
jdogresorg Sep 21, 2026
4e834f8
Merge: docs(decoder): preserve rewritten comment detail
jdogresorg Sep 21, 2026
c384a93
chore(observability): vendor the hub canonical module, unmatched-rout…
jdogresorg Sep 22, 2026
003848e
build(ci): grade the fast tier on a push, defer the heavy tiers to th…
jdogresorg Sep 22, 2026
b1d58c0
test(decoder): add coverage for db/query_helpers
jdogresorg Sep 22, 2026
12f7841
Merge: test(decoder): add coverage for db/query_helpers
jdogresorg Sep 22, 2026
92e80d4
test(decoder): pin the 120 s shutdown contract against the tracker's …
jdogresorg Sep 23, 2026
8be5315
fix(decoder): pin the undo ceiling to the dispenser safe depth, corre…
jdogresorg Sep 23, 2026
1a25b76
test(decoder): resolve the manifest byte-identity guard through the s…
jdogresorg Sep 23, 2026
ace01ef
test(decoder): cover reorg halt reconciliation call sites
jdogresorg Sep 23, 2026
30063d8
test(decoder): avoid a new underscored hook lint finding
jdogresorg Sep 23, 2026
658ddfb
Merge: test(decoder): avoid a new underscored hook lint finding
jdogresorg Sep 23, 2026
d9bf9b4
chore(release): v0.20.1
jdogresorg Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .ci-siblings
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ xchain-hub
xchain-indexer
# AuxPoW strip parity and the dispenser safe-depth twin.
xchain-utxo-tracker
# The shutdown drain staying under the stop budget xchain-node gives a decoder.
xchain-node
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
# touch neither the guard nor the file it reads. Empty everywhere
# else, which keeps the existing behaviour. Same expression as
# xchain-indexer and xchain-explorer.
siblings-ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || '' }}
siblings-ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || github.base_ref || '' }}

# Cross-repo drift guards: this repo vendors the canonical coin registry from
# xchain-hub, and per-repo CI cannot see sibling repos, so a drifted vendored
Expand All @@ -47,7 +47,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: XChain-Platform/xchain-hub
ref: ${{ github.ref == 'refs/heads/master' && 'master' || 'develop' }}
ref: ${{ github.base_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
ssh-key: ${{ secrets.XCHAIN_HUB_DEPLOY_KEY }}
path: xchain-hub

Expand Down Expand Up @@ -134,7 +134,7 @@ jobs:
# whose cross-repo guards disagree with the gate for a reason that exists
# nowhere but in CI.
with:
ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || '' }}
ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || github.base_ref || '' }}

- name: Use Node.js 22
uses: actions/setup-node@v4
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.20.1] - 2026-09-23

### Fixed
- Raised the Litecoin testnet rollback window and pinned decoder undo depth to the dispenser safe depth.


## [0.20.0] - 2026-09-17

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# XChain Platform Decoder

<p align="center">
<img src="https://img.shields.io/badge/version-0.20.0-blue" alt="Version">
<img src="https://img.shields.io/badge/version-0.20.1-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C118%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/node-%3E%3D22-green" alt="Node">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue" alt="License">
Expand Down
44 changes: 42 additions & 2 deletions bin/ci-full.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,35 @@ SELF="$(pwd)"
SIB="$(cd .. && pwd)"

FAILED=""
# >>> ci-tier (generated block; re-run the tier wirer to update) >>>
# Tier classes. A push grades the FAST tier only: the unit job, the pin and
# drift guards, and the structure and hygiene checks the hook runs before it
# dispatches. The tiers named below (coverage re-runs, perf scenarios) are
# skipped when the gate sets CI_TIER=fast, and each skip is recorded so the
# closing verdict can never claim a green it did not earn. Nothing stops
# being graded: a scheduled sweep re-runs this same script with CI_TIER=full
# on every repo every three hours and before any release or deploy, and a
# red there is tracked down and fixed first. CI_TIER is unset for a hand
# run, so a bare `npm run ci:full` still runs every tier as it always did.
CI_TIER_FULL_ONLY=(
"coverage ratchet (coverage:check)"
)
DEFERRED=""
ci_tier_deferred() {
[ "${CI_TIER:-full}" = "fast" ] || return 1
local t
for t in ${CI_TIER_FULL_ONLY[@]+"${CI_TIER_FULL_ONLY[@]}"}; do
if [ "$t" = "$1" ]; then
DEFERRED="$DEFERRED [$1]"
echo; echo "ci:full ===== $1 DEFERRED (CI_TIER=fast, runs in the full sweep) ====="
return 0
fi
done
return 1
}
# <<< ci-tier <<<
run_tier() {
ci_tier_deferred "$1" && return 0 # ci-tier guard (generated)
local name="$1"; shift
echo; echo "ci:full ===== $name ====="
if "$@"; then
Expand Down Expand Up @@ -91,7 +119,7 @@ need_docker() {
}
}

need_sib xchain-encoder xchain-documentation xchain-hub xchain-indexer xchain-utxo-tracker
need_sib xchain-encoder xchain-documentation xchain-hub xchain-indexer xchain-utxo-tracker xchain-node

# --- job: ci (XChain-Platform/.github ci-reusable.yml -> npm run ci) -------
run_tier "ci" npm run ci
Expand Down Expand Up @@ -129,8 +157,20 @@ run_tier "docker: end-to-end tier (test:e2e)" npm run test:e2e
run_tier "coverage ratchet (coverage:check)" npm run coverage:check

echo
# >>> ci-tier summary (generated) >>>
echo "ci:full: tier class ${CI_TIER:-full}"
if [ -n "${DEFERRED:-}" ]; then
echo "ci:full: DEFERRED to the full sweep:$DEFERRED"
fi
# <<< ci-tier summary <<<
if [ -n "$FAILED" ]; then
echo "ci:full: RED tiers:$FAILED"
exit 1
fi
echo "ci:full: all tiers green (same set GitHub CI runs)"
# >>> ci-tier verdict (generated) >>>
if [ "${CI_TIER:-full}" = "fast" ]; then
echo "ci:full: all FAST tiers green; the DEFERRED tiers above were NOT graded here"
else
echo "ci:full: all tiers green (same set GitHub CI runs)"
fi
# <<< ci-tier verdict <<<
102 changes: 102 additions & 0 deletions bin/fixture-ports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const REPO = path.join(__dirname, '..');
const SPECS = Object.freeze({
'test/integration/fixtures/docker-compose.test.yml': Object.freeze({
XCHAIN_TEST_NODE_PORT: 18544,
XCHAIN_TEST_DB_PORT: 13318
}),
'test/e2e/fixtures/docker-compose.test.yml': Object.freeze({
XCHAIN_E2E_NODE_PORT: 18545,
XCHAIN_E2E_DB_PORT: 13319
})
});

function integer(value, label) {
const text = String(value);
if (!/^(0|[1-9][0-9]*)$/.test(text)) {
throw new Error(`${label} must be a non-negative integer, got ${JSON.stringify(text)}`);
}
return Number(text);
}

function offset(env) {
const source = env || process.env;
return source.CI_PORT_OFFSET === undefined || source.CI_PORT_OFFSET === ''
? 0
: integer(source.CI_PORT_OFFSET, 'CI_PORT_OFFSET');
}

function basePort(name) {
for (const spec of Object.values(SPECS)) {
if (Object.prototype.hasOwnProperty.call(spec, name)) return spec[name];
}
throw new Error(`unknown fixture port ${name}`);
}

function port(name, env) {
const source = env || process.env;
const value = source[name] === undefined || source[name] === ''
? basePort(name) + offset(source)
: integer(source[name], name);
if (value < 1 || value > 65535) throw new Error(`${name} resolves outside the TCP port range: ${value}`);
return value;
}

function composeEnvironment(file, env) {
const spec = SPECS[file];
if (!spec) throw new Error(`unknown fixture compose file ${file}`);
const result = { ...(env || process.env) };
for (const name of Object.keys(spec)) result[name] = String(port(name, result));
return result;
}

function render(file, env) {
const spec = SPECS[file];
if (!spec) throw new Error(`unknown fixture compose file ${file}`);
let source = fs.readFileSync(path.join(REPO, file), 'utf8');
for (const [name, base] of Object.entries(spec)) {
source = source.split(`\${${name}:-${base}}`).join(String(port(name, env)));
}
return source;
}

function compose(file, args, env) {
const result = spawnSync('docker', ['compose', '-f', file, ...args], {
cwd: REPO,
env: composeEnvironment(file, env),
stdio: 'inherit'
});
if (result.error) throw result.error;
return result.status === null ? 1 : result.status;
}

function main(argv) {
const [command, name, ...args] = argv;
if (command === 'render' && name && args.length === 0) {
process.stdout.write(render(name));
return 0;
}
if (command === 'port' && name && args.length === 0) {
process.stdout.write(String(port(name)) + '\n');
return 0;
}
if (command === 'compose' && name && args.length > 0) return compose(name, args);
throw new Error('usage: fixture-ports.js <render FILE|port NAME|compose FILE ARGS...>');
}

if (require.main === module) {
try {
process.exitCode = main(process.argv.slice(2));
} catch (err) {
console.error(err.message);
process.exitCode = 1;
}
}

module.exports = { SPECS, offset, port, composeEnvironment, render };
2 changes: 1 addition & 1 deletion bin/pins/identity.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"src/coins/index.js": "dd350bc0ec999849fe302ac4381f37b7be3eaa866f019a35cbcceebb4d5ebd4b"
},
"twinFixtures": {
"test/fixtures/action-manifest.json": "93ac85b4d76f078951a2e95eb3ca303f39fb9f18d0fb91b1ae16693716bef72f",
"test/fixtures/action-manifest.json": "05813d667976ec49bdeb001fc7f5aea41fce4f343a37576a2716d176b334a823",
"test/fixtures/roundtrip-conformance.json": "d9963ac7c35bdcfab99595fec040d7791d73119cb62fef20db136f8ea56236ed"
}
}
6 changes: 6 additions & 0 deletions bin/pins/suite-title-splits.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
"test/unit/dispenser_cancel_grace.test.js",
"test/unit/dispenser_cancel_grace.test/01_database_get_all_open_dispenser_addresses_grace_floor.test.js"
],
"test/unit/decoder_halt_diagnostics.test.js": [
"test/unit/decoder_halt_diagnostics.test.js",
"test/unit/decoder_halt_diagnostics.test/01_reorg_halt_records.test.js",
"test/unit/decoder_halt_diagnostics.test/02_health_probe_records.test.js",
"test/unit/decoder_halt_diagnostics.test/03_db_cleanup_record.test.js"
],
"test/e2e/action_decoding.test.js": [
"test/e2e/action_decoding.test.js",
"test/e2e/action_decoding.test/01_encoding_types.test.js",
Expand Down
9 changes: 5 additions & 4 deletions bin/run-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ cd "$(dirname "$0")/.." || exit 1

COMPOSE_FILE="test/e2e/fixtures/docker-compose.test.yml"
KEEP_VENUE="${KEEP_VENUE:-0}"
COMPOSE=(node bin/fixture-ports.js compose "$COMPOSE_FILE")

teardown() {
if [ "$KEEP_VENUE" = "1" ]; then
echo "[e2e] KEEP_VENUE=1, leaving the venue up ($COMPOSE_FILE)"
return
fi
echo "[e2e] Tearing down the venue"
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1
}
trap teardown EXIT

Expand All @@ -47,12 +48,12 @@ if ! command -v docker >/dev/null 2>&1; then
fi

# Start from a clean slate even if a previous run was killed before its teardown.
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1

echo "[e2e] Bringing up the venue"
if ! docker compose -f "$COMPOSE_FILE" up -d --wait; then
if ! "${COMPOSE[@]}" up -d --wait; then
echo "[e2e] Venue failed to become healthy" >&2
docker compose -f "$COMPOSE_FILE" ps
"${COMPOSE[@]}" ps
exit 1
fi

Expand Down
9 changes: 5 additions & 4 deletions bin/run-integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ cd "$(dirname "$0")/.." || exit 1

COMPOSE_FILE="test/integration/fixtures/docker-compose.test.yml"
KEEP_VENUE="${KEEP_VENUE:-0}"
COMPOSE=(node bin/fixture-ports.js compose "$COMPOSE_FILE")

teardown() {
if [ "$KEEP_VENUE" = "1" ]; then
echo "[integration] KEEP_VENUE=1, leaving the venue up ($COMPOSE_FILE)"
return
fi
echo "[integration] Tearing down the venue"
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1
}
trap teardown EXIT

Expand All @@ -47,12 +48,12 @@ if ! command -v docker >/dev/null 2>&1; then
fi

# Start from a clean slate even if a previous run was killed before its teardown.
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1

echo "[integration] Bringing up the venue"
if ! docker compose -f "$COMPOSE_FILE" up -d --wait; then
if ! "${COMPOSE[@]}" up -d --wait; then
echo "[integration] Venue failed to become healthy" >&2
docker compose -f "$COMPOSE_FILE" ps
"${COMPOSE[@]}" ps
exit 1
fi

Expand Down
Loading
Loading