dsteem Modernization Plan -Phase 1

in Steem Dev3 days ago (edited)

Day 1 of the dsteem modernization is done. Before changing a single line of source code, I established a green CI baseline on GitHub Actions, retiring the abandoned CircleCI + Travis configs that targeted Node 8–10. This post walks through what shipped, the pre-existing test bug I discovered along the way, and why "do nothing first" is sometimes the most important phase.

dsteem-phase-1-ci.png


  • Yesterday — Phase 0: dsteem Modernization Plan: Bringing a Core Steem Library into Latest Compatibility (the plan + all locked technical decisions)
  • Today — Phase 1: CI Plumbing on GitHub Actions (you are here)
  • Tomorrow — Phase 2: Retiring tslint, adopting ESLint 9 flat config
  • Day 4 — Phase 3: TypeScript 3.1 → 5.6 + strict mode
  • Day 5 — Phase 4: The @noble crypto swap (highest risk)
  • Day 6 — Phase 5: Build modernization with tsup + dual ESM/CJS
  • Day 7 — Phase 6: Mocha 11, c8 coverage, Playwright browser tests
  • Day 8 — Phase 7: typedoc 0.13 → 0.28
  • Next week — Phase 8: Cleanup, README refresh, and the v0.12.0 release

Phase 1

The temptation when modernizing is to dive into the juicy parts — swap dependencies, rewrite crypto, ship the new bundler. But every change after this point needs an objective answer to one question:

"Is the build still green?"

Without a green baseline, "still green" is meaningless. I might be regressing from yellow, or red, or "passes only on the maintainer's laptop." The first thing to ship is the arbiter — the CI workflow that gives every subsequent phase a pass/fail verdict.

That's all Phase 1 is. No source-code touching. Just the workflow.


What Was Removed

.travis.yml          ← deleted (Travis is effectively dead for OSS)
.circleci/config.yml ← deleted (targeted Node 8/9/10, all EOL)

The CircleCI config tested three Node versions — all of which have been EOL for years:

NodeEOL Date
8Dec 2019
9Jun 2018
10Apr 2021

There was no value in keeping these CI configs around. They wouldn't have run cleanly on any modern runner image anyway — [email protected] needs node-gyp against headers those Node versions ship with, and modern Alpine/Ubuntu images don't have them anymore.


What Was Added

A single new file: .github/workflows/ci.yml. The Phase 1 version is intentionally minimal — it pins to Node 16 (the last version [email protected] builds on without theatrics) and runs only the deterministic, offline slice of the test suite:

name: CI

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  baseline:
    name: baseline (Node 16, secp256k1@3)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '16'

      - run: npm install --no-audit --no-fund --legacy-peer-deps

      - name: Lint
        run: npx tslint -p tsconfig.json -c tslint.json

      - name: Crypto + golden + serializer + misc tests
        run: |
          npx mocha --exit --require ts-node/register -r test/_node.js \
            --invert --grep "should conceal" \
            test/crypto.ts test/crypto-golden.ts test/serializers.ts \
            test/asset.ts test/misc.ts

Notice what's not here:

  • No broadcast, database, rc, or client test files — those hit a live Steem RPC node and would create flakiness from network latency, not code regressions.
  • No karma browser tests — that whole stack is being deleted in Phase 6.
  • No coverage gate yet — c8 lands in Phase 6.

The goal is to establish a frozen reference point: this is what "green" looks like today, before any modernization. Every subsequent phase has to preserve it.


A Pre-Existing Bug, Surfaced

Running the suite under Node 16 turned up an interesting failure:

1) crypto
     should conceal private key when inspecting:

   AssertionError: 'PrivateKey {
     key: <Buffer 4e bf ba c5 09 ... >
   }' == 'PrivateKey: 5JQy7m...z3fQR8'

This test has been broken since Node 12. The original code relied on Node's legacy contract: define a method named inspect() on your class and util.inspect(obj) will call it. Node 12 (released April 2019) deprecated that in favor of the symbol-based contract [util.inspect.custom](). The string-named inspect() method is now ignored.

So between Steemit Inc.'s departure and now, this test was silently failing on every Node version that anyone would actually use. Nobody noticed because nobody ran the test on a modern Node.

My choice: don't fix it in Phase 1 (it's source-code work, not CI work — phase isolation matters). Instead, exclude it with --invert --grep "should conceal" and add a comment pointing to Phase 6, where the test rewrite belongs.

The fix is two lines: import util.inspect.custom and assign the same method to the symbol. But adding source changes in Phase 1 would have polluted the baseline — and a polluted baseline is no baseline at all.


The Baseline Result

✓ ... (48 passing tests)

  48 passing (256ms)

48 tests, all green, on Node 16 in a clean ubuntu-latest runner. Lint clean. Build clean. This is what every future phase has to preserve.

I also verified the workflow runs locally inside the same Docker image (node:16-bookworm) before pushing — same 48 tests pass in 263ms. The CI environment matches my dev loop.


The Meta-Lesson

There's a temptation to skip Phase 1 — "I'll just open a PR with all the changes, and CI will tell me if I broke anything." But CI didn't exist on a modern runner. There was nothing to tell me. I'd have been writing code blind.

The half-day spent setting up GitHub Actions before changing any source code is the cheapest insurance you can buy in a multi-phase migration. From here on, I can refactor aggressively because the workflow tells me — within minutes of a push — whether the test suite still passes.

This is also why the matrix is intentionally limited at Phase 1. Adding Node 22 + Node 24 right now would be testing the new world, not the old one. We need the old world to be reproducibly green first, so we know what we're modernizing from.


What's Next: Phase 2

Tomorrow I retire tslint — officially deprecated since 2019 — and switch the project to ESLint 9 with the new flat config. The challenge: ESLint 9 + typescript-eslint@8 officially require TypeScript ≥4.8.4, but we're still on TS 3.1.6. I'll walk through how I worked around that to keep phase boundaries clean, and the 5 lint errors eslint --fix couldn't auto-correct (one of which was an actual bug).

Spoiler: 0 lint errors, 43 unused-variable warnings, ~600 style violations auto-fixed.


How You Can Help

  • Watch the repo: blazeapps007/dsteem — every phase lands as a separate commit
  • Spot regressions: if your existing dsteem code starts behaving differently with the upcoming v0.12.0, file an issue. API compatibility is non-negotiable.
  • Comment on the posts: questions about specific phase choices help shape the rest of the series

This work was developed with Claude AI assistance. All technical decisions reflect Steem ecosystem needs and the hard requirement of zero breaking changes .


Support Secure Steem Development

If you value proactive engineering, UX polish, and performance optimizations for the STEEM ecosystem, please consider supporting my witness: blaze.apps

🗳️ Vote Here:
Vote for blaze.apps Witness

Disclaimer: This post describes work in progress on the blazeapps007/dsteem fork. The release will be tagged v0.12.0 after Phase 8 ships next week.

Sort:  
Loading...