Growing Your Project with UmtriPart 4

Logging Deploys, Refactoring, and Bugs to Umtri

How to auto-log deployments with GitHub Actions, sync the tree after refactoring, and map and register a bug's scope of impact through the tree.

Once you’ve got the hang of stacking features and recording them in the tree, it’s time to also capture the events that happen after development — deployments, refactoring, and bugs — in Umtri. As these records build up, you can answer questions like “which deployment introduced this bug?”


Automatically Logging Every Deployment (GitHub Actions)

Once a deployment succeeds, it’s worth logging that fact to your Umtri timeline. Building up a record of when you deployed what makes it much easier later to trace problems.

Doing this by hand is tedious. With GitHub Actions, you can automatically log to Umtri every time you push code to GitHub.

What Is GitHub Actions?

GitHub Actions is a feature that automatically runs tasks whenever you push code to a repository or certain other events happen. It’s commonly used to automate things like running tests, building, and deploying.

How a Commit Attaches to the Tree

Before we get to the setup, it’s worth knowing how this record connects to your nodes.

Umtri doesn’t run jobs or analyze code in your CI. Your CI sends the commit sha and the list of files that commit changed, and the server matches those files against each node’s metadata.implements — the files that realized that node. If any of a node’s files appear in the commit, the commit is added to that node’s history; nodes with no matching files are left alone. That’s why unrelated commits don’t pollute the tree.

The result is a tree where every node carries the actual commits that built it.

Order matters here. Recording a commit is always the last step. If this round of work produced a new unit, create the node first (with the file paths in metadata.implements); if you moved a file, fix that node’s implements first. Get the order backwards and the commit lands only on the nodes that happened to already exist, while the part you just built stays invisible in the tree. Bringing the tree back in line with the code is covered in the refactoring section below.

Setting Up Automatic Umtri Logging

Create a .github/workflows/ folder at the root of your project, and save a file called umtri-record-commit.yml with the following contents.

name: Record commit to Umtri

on:
  push:
    branches: [main]

jobs:
  record:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Record commit to Umtri
        env:
          UMTRI_PAT: ${{ secrets.UMTRI_PAT }}
          UMTRI_GROUND_SLUG: ${{ vars.UMTRI_GROUND_SLUG }}
          BEFORE_SHA: ${{ github.event.before }}
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
        run: |
          # Skip quietly if it isn't configured yet
          if [ -z "$UMTRI_PAT" ] || [ -z "$UMTRI_GROUND_SLUG" ]; then
            echo "Umtri isn't configured — skipping."
            exit 0
          fi

          # Files changed in this push (fall back to a single commit on a new branch)
          if git cat-file -e "$BEFORE_SHA^{commit}" 2>/dev/null; then
            FILES=$(git diff --name-only "$BEFORE_SHA" "$GITHUB_SHA")
          else
            FILES=$(git diff --name-only "$GITHUB_SHA^" "$GITHUB_SHA")
          fi

          if [ -z "$FILES" ]; then
            echo "No changed files."
            exit 0
          fi

          printf '%s\n' "$FILES" \
            | jq -R . \
            | jq -sc --arg sha "$GITHUB_SHA" --arg message "$COMMIT_MESSAGE" \
                '{sha: $sha, message: $message, files: .}' \
            | curl -sS -X POST \
                "https://api.umtri.io/api/projects/$UMTRI_GROUND_SLUG/record-commit" \
                -H "Authorization: Bearer $UMTRI_PAT" \
                -H "Content-Type: application/json" \
                -d @-

Then register two things under Settings → Secrets and variables → Actions in your GitHub repository. Note that one is a Secret and the other is a Variable — they’re different kinds.

NameKindValue
UMTRI_PATSecretAn Umtri PAT issued with the write scope (used as the Bearer token)
UMTRI_GROUND_SLUGVariableThe ground slug for this repository (e.g. bookmark-app)

Always keep the token as a Secret and never write it directly into YAML that gets committed. If it’s exposed, revoke it in settings at app.umtri.io and issue a new one.

Once this is set up, every push to main records that commit against the relevant nodes. If either value is missing, the workflow skips the API call instead of calling it, so your CI won’t go red just because you haven’t configured it yet.

The endpoint is idempotent by sha. Sending the same commit more than once won’t stack duplicates on a node, so re-running the workflow is safe.

There’s a side benefit as these records accumulate. When a single commit touches several nodes that aren’t connected to each other yet, the response comes back with those pairs flagged as dependency candidates — things that always change together usually have a relationship. It won’t connect them for you, so if the relationship is real, go add the edge yourself.

You can also just ask Claude Code to create this file for you.

Create a .github/workflows/umtri-record-commit.yml file.
On every push to main, it should collect the list of changed files and POST them to
https://api.umtri.io/api/projects/<slug>/record-commit
Read the token from the UMTRI_PAT secret and the slug from the UMTRI_GROUND_SLUG variable.

If you’d rather not wire up CI, the same thing is available over MCP as the record_commit tool — just ask Claude Code to log the commit once you’re done working.

What CI Can and Can’t Be Trusted With

CI is a good place for recording because it never forgets a SHA. It’s a poor place for judgment. By the time the workflow runs, the commit already exists, nobody reads the log, and the run is green either way. Whether to create a new node, whether to fix the implements of a moved file, whether this commit closes a bug — those need deciding while a person or an AI is still in the loop.

That’s why Umtri recommends writing the habit into your repository’s own rules file (CLAUDE.md, AGENTS.md, whatever that project uses). It’s the file every session loads on its own.

### Umtri sync (before committing)

Umtri does not read git. Before making a commit, check whether the change also
belongs in the tree:

1. Compare the changed paths against nodes' `metadata.implements`.
   - New unit of the system (route, screen, table, integration) → `create_node`
     with `metadata.implements` set. Wire its connections too
     (`create_edge` / `create_api`) — an unconnected node is invisible to impact.
   - File moved or renamed → `update_node` on the affected nodes' `implements`.
   - Behavior changed but structure did not → nothing to do.
2. Does the commit close a tracked bug? → `update_bug` (status, and `solution`
   describing what was actually done).
3. Then `record_commit` with the SHA and the changed paths.

Ask the human before adding nodes for anything ambiguous — a wrong node is
harder to notice later than a missing one.

With that in place, CI handles step 3 automatically, and steps 1 and 2 get caught right where the commit is being made.


Reflecting Refactoring Results in the Tree

Refactoring changes structure rather than adding new features, so your Umtri tree needs to be updated to match the new structure too.

The refactoring is done. Update the Umtri bookmark-app tree as follows:

- Add storage.js as a new leaf node (under the BookmarkList twig)
- Add useBookmarks.js as a leaf under the hooks twig
- Update the App.jsx leaf description to "handles UI layout only"
- Remove the node related to the deleted loadData() function

When your tree stays in sync with the code, later on you can just ask the AI “what does the current structure look like?” — pulling up the Umtri tree alone gives you an accurate picture of the current state.


Mapping a Bug’s Scope of Impact with the Tree

Once you’ve found the root cause of a bug, you need to figure out how far its impact reaches. A single bug can affect multiple features at once.

For example, if there’s a problem with how data is saved to localStorage, it could affect not just bookmark storage but every feature that uses localStorage — tag storage, settings storage, and so on.

There’s a dedicated tool for working out this scope: get_impact. Instead of pulling the whole tree and eyeballing it, it starts from a single node and walks the edges and API connections to answer “if this breaks, how far do I need to check?”

Check the scope of impact for the storage.js node in Umtri bookmark-app.
If the localStorage save logic breaks, which features do I need to check?

One thing worth flagging: impact does not flow in the direction of the arrows. Dependency edges are followed backward (target → source), data flow edges forward, and APIs from callee back to caller. If “A depends on B,” then it’s A that hurts when B breaks. If you want the opposite — “what does this node rely on?” — pass the direction as dependsOn.

And the result is a list of things to check, not a proven set of damage. It’s only as complete as the connections you’ve recorded. In particular, if the starting node has no connections at all, the result comes back empty — and that means “nothing recorded,” not “nothing affected.” In that case, go record the edges first.


Registering the Bug in Umtri

Registering a bug you found in Umtri lets you later track “what bug existed when, and how it was fixed.”

With the MCP connection active, ask Claude Code something like this.

Register the bug below under Umtri bookmark-app.

Title: Deleted bookmark reappears after refresh
Target node: storage.js (leaf)
Risk score: 5
Status: open
Fix direction: looks like the localStorage update is missing on delete —
check the save call in the delete handler

A bug attaches to a node or an API, and for a problem you can’t pin to any one node, it can attach to the project as a whole.

Instead of severity labels like high or medium, Umtri uses a risk score from 0 to 8. It blends functional impact with how hard the fix is; the default is 4 and 8 is the riskiest. Urgency is deliberately left out — that’s for a person to decide in context.

ScoreMeaning
0–1No functional impact (an idea, a wording change)
2Not an error, but usability may change
3–4Minor functional issue (simple / complex fix)
5–6Significant functional issue (simple / complex fix)
7–8Critical issue (simple / complex fix)

Status moves one step at a time: open → in_progress → resolved. in_progress in particular is the only marker that says “someone is already on this,” so it’s worth setting the moment you start the fix, not after it lands. It keeps another session or another person from digging into the same bug in parallel.

What you wrote as Fix direction goes into the solution field. At report time it’s a plan — “this is probably how we fix it” — and once it’s actually fixed, you overwrite it with what you really did. It’s the same field, rewritten as understanding changes, and it’s the field you’ll end up reading when something similar shows up later. Leaving a stale plan behind is worse than leaving it empty, so if you don’t know yet, leave it blank.

Once it’s registered, the response comes back with the node’s scope of impact attached — which nodes it reaches, how many hops away they are, which connection it traveled through, and any other active bugs already sitting inside that radius. So you don’t have to call get_impact separately: registering the bug also tells you how far the QA needs to go.


Coming Up Next

In the final post, we’ll pull together how these accumulated records — the tree, the timeline, and the bug history — make a difference whether you’re solo or on a team, and why Umtri’s visualization is so useful.