MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3750

8.45 `motir-marketing` publishes no DESIGN RESULT — it has no publish lane at all, and a `<name>.design-notes.md` basename is invisible to the classifier in either repo

Done
Description

Found by motir run MOTIR-1143, which delivered the first design asset ever to land in motir-marketing and produced no design result on its card. There are two independent causes, and fixing either one alone still leaves the card empty.

▶ FULLY DELIVERED 2026-08-28 — three pull requests, and the last one is motir-core's. All six criteria below are now struck through or dispositioned with their evidence.

halfPR
the publish lane in motir-marketingmotir-marketing#3 (merged 11:24Z)
the keyless id-token: write, once MOTIR-3743 connected the repomotir-marketing#4 (merged 11:52Z)
classifyDesignPath's note arm + the ADR amendmentmotir-core#2418 — the pull request LINKED to this card

⚠️ Closes MOTIR-3750 in motir-marketing#3's body is an OVERCLAIM — that pull request delivered a proper subset of this card, and so did #4. Neither is linked to it; motir-core#2418 is the one that completes it.

Cause 1 — the repository has no publish lane · FIXED

The design-result publisher is the Publish the design result step of motir-core's ci.yml design-guards job, running scripts/upload-design-assets.mjs. motir-marketing's CI was lint · typecheck · build · deploy and had no equivalent. A design asset merged there reached no card at all.

The script is portable, which is what made this fixable rather than a rewrite: it imports only node:child_process, node:fs and node:path plus global fetchzero external dependencies — so bare node runs it in any checkout. It is FETCHED from motir-core (public repo) pinned to a commit SHA, never forked.

And the PAT path is repository-AGNOSTIC, which is the discovery that makes this work at all: authenticateCiPublisher tries keyless GitHub OIDC first and falls back to authenticateApiToken(req, DESIGN_PUBLISH_PERMISSION), where DESIGN_PUBLISH_PERMISSION = 'work_item:edit'. That path performs no repository check — it authenticates the token, resolves its workspace, and resolves the card by identifier.

⚠️ The keyless path canNOT be used until MOTIR-3743 lands, and turning it on early is RED, not inert. ⚠️ SUPERSEDED 2026-08-28 — MOTIR-3743 IS done. The reasoning above was correct at the time and is kept because it is the trap: authenticateGithubOidc resolves the tenant from the verified repository claim via githubRepoRepository.findConnectedByName(owner, name) and returns 403 repo_not_connected for an unconnected repo, and the script PREFERS OIDC whenever it can mint one — so id-token: write on an unconnected repository is a red check on every pull request, not a quiet skip. The repository is now connected, so motir-marketing#4 turned the line on and the lane is keyless. The PAT arm stays wired and deliberately UNSET as the fork-PR fallback, where GitHub denies OIDC.

Cause 2 — the note's BASENAME, and it is not only this repository's problem

classifyDesignPath in scripts/upload-design-assets.mjs:

if (filePath.endsWith('.mock.html')) return 'mock';
if (filePath.endsWith('.png')) return 'image';
if (path.basename(filePath) === NOTES_BASENAME) return 'note';   // 'design-notes.md'
return null;

The mock and the export match on SUFFIX; the note matches on exact basename. So a file named <name>.design-notes.md classifies as null, lands in ignored, and is never published — while its sibling .mock.html and .png publish normally. The failure is silent and partial: the card gets pictures and no words.

Measured on motir-core origin/main (2026-08-28), and the predicate is "every .md under design/ that is not exactly design-notes.md", not a grep for one name:

git ls-tree -r --name-only origin/main design/ | grep -E '\.md$' | grep -v -E '/design-notes\.md$'
  → design/org-admin/create-workspace.design-notes.md      (1)
git ls-tree -r --name-only origin/main design/ | grep -cE '/design-notes\.md$'
  → 43     ⚠️ CORRECTED — this card said 44

⚠️ TWO CORRECTIONS TO THIS SECTION, made by the run that fixed it (motir-core#2418). Neither changes the fix; both are on the record because a number and a cause were asserted and neither held.

1. The denominator was 43, not 44. Re-run with the card's own command: 43 at a482d16b8 (today's origin/main) AND 43 at 6e71acf21, the commit motir-marketing's lane pins — and git log --diff-filter=DR finds no design-notes.md deleted or renamed in the window, so the number did not drift; it was one high when written. The load-bearing half — exactly ONE non-conforming file — reproduces exactly.

2. "That one file's notes have never reached their card either" is TRUE, but NOT because of the classifier. design/org-admin/create-workspace.design-notes.md was last touched 2026-08-10 (#2004, MOTIR-2542); scripts/upload-design-assets.mjs first landed 2026-08-12 (cacfe0180, MOTIR-2664). The publisher is pull_request-only, and no pull request has touched that file in the two days since the publisher existed — so the mechanism was never there to drop it. The classifier has cost that file nothing yet; it would have, on its next edit.

Acceptance criteria

  • motir-marketing runs the design-result publisher on every pull_request, using motir-core's script rather than a fork of it, and the mechanism for getting it there is stated in the workflow with the reason. MET — motir-marketing#3, merged 2026-08-28. Fetched from motir-core@6e71acf21c1426180bf502c387e221108930e381 (checked before trusting: 977 lines, anonymous fetch, node --check parses, diff vs origin/main byte-identical).
  • The lane is green and inert with no credential configured, and does not carry id-token: write while MOTIR-3743 is open. A comment names the one line to add once it closes. MET — observed on motir-marketing#3's own run: Fetched publisher from motir-core@6e71acf… / Nothing to publish — this PR changed no design artifact. / Design result pass 11s. The comment named the line; motir-marketing#4 added it once MOTIR-3743 closed.
  • The credential is a named MANUAL step, not an assumption: a Motir API token granted work_item:edit, stored as the MOTIR_UPLOAD_TOKEN repository secret on moooon-B-V/motir-marketing. SUPERSEDED, not delivered — and by the better fix this criterion itself named. MOTIR-3743 is done, the repository is connected, and motir-marketing#4 made the lane keyless. No credential was created and none is owed. The PAT arm stays wired and unset as the fork-PR fallback only.
  • Verified by OUTPUT, not by a green check: the job log reads Published N design artifact(s) to MOTIR-<n> … evidence <id>, and that evidence id is on the card. MET, on the real asset, keyless, in CI. motir-marketing PR #2, run 33171127277, job Design result, 2026-08-28T12:27:18Z:
    Design note: 11 changed section(s) from design/marketing/design-notes.md.
    Published 3 design artifact(s) to MOTIR-1143 (target from branch); evidence cmtcxfev000b2hwn8sw8kuuja.
    
    That line prints only after POST /api/work-items/MOTIR-1143/design-evidence returns 2xx; the id is the row the server created, which is the row designEvidenceService.getCurrentForWorkItem feeds to the item page's Design result panel. Three assets were PUT to their signed tokens first, and a failed PUT throws. Stated limit: the design result has no GET route (POST + DELETE only), so its only consumer surface is the authenticated item page, which this run could not render. The evidence is the server's own confirmation, not a page render.
  • classifyDesignPath's note arm either accepts a *.design-notes.md suffix as it accepts *.mock.html, or the one non-conforming file in motir-core is renamed and a guard in the design lane keeps the convention. Decide which; do not do both. DECIDED: WIDEN the note arm (motir-core#2418), and the rename was rejected on shipped reality rather than taste — design/org-admin/ already carries BOTH design-notes.md (the area index) AND create-workspace.design-notes.md, so the rename is a content MERGE of two spec files, and it leaves the trap armed for the next author. The losing name is also the one CLAUDE.md's three-file rule literally prescribes ("land all three, with a shared basename"), which is why two authors wrote one ten weeks apart in two repositories. tests/design-assets-uploader.test.ts covers the new arm in three cases, including that the . separator is load-bearing. docs/decisions/design-result.md §1 is amended (AMENDMENT 1) because its table documented the exact basename as the contract. The per-area convention is unchanged and still enforced by tests/design-three-file-set.test.ts.
  • design/org-admin/create-workspace.design-notes.md's own result is republished or its absence recorded, whichever the decision above implies. ABSENCE RECORDED — in design-result.md AMENDMENT 1 and in correction 2 above. The widening implies it: the file's last edit predates the publisher, so nothing was ever dropped, and the next pull request that touches it publishes it — the first time any pull request could have. Republishing by hand was considered and rejected: resolveTargetKey routes on the first resolvable MOTIR-<n> in DESIGN_TARGET_KEY → the branch ref → the PR title, so touching that file on the fixing branch would have published org-admin's spec onto MOTIR-3750 — the drive-by-asset hazard, arriving green with a real evidence id on a card nobody would think to check.

⚠️ ORDERING — merging motir-marketing#2 early COSTS MOTIR-1143 its design result

The publish step is pull_request-only, deliberately (motir-core does the same, so an identical result is not re-published after the merge — design-result.md §6). On push: main the job's if is false and it is skipped. So there is no post-merge path: if motir-marketing#2 merges before a credential exists, no CI run ever publishes MOTIR-1143's asset.

▶ DISCHARGED — the ordering held. #3 and #4 both merged before #2, #2's next run picked up the keyless lane, and it published (the fourth criterion above). The mechanism it describes is unchanged and still true of any future asset: there is no post-merge path, so a design asset in motir-marketing publishes from its own pull request or not at all.

Context refs

  • motir-core/scripts/upload-design-assets.mjsclassifyDesignPath, main's four exit-0 conditions, resolveTargetKey's DESIGN_TARGET_KEY override, and authHeadersFor's OIDC-before-PAT preference.
  • motir-core/lib/github/oidcAuth.tsfindConnectedByName403 repo_not_connected, the reason the keyless path waited on MOTIR-3743.
  • motir-core/lib/publishAuth/ciPublishAuth.ts + lib/tokens/grant.ts — the PAT path and DESIGN_PUBLISH_PERMISSION = 'work_item:edit'.
  • motir-marketing/.github/workflows/ci.yml — the shipped design-result job and the reasoning in its comments.
  • motir-core/docs/decisions/design-result.md — what a design result is and where it attaches; §1 + AMENDMENT 1.
  • MOTIR-3743 — the repository connection; closing it unlocked the keyless path and removed the secret.
  • MOTIR-1143 — the asset that surfaced both causes, and the fixture the OUTPUT check was measured on.
  • MOTIR-3724 — the precedent for the one thing this card deliberately did NOT decide: where a mechanism shared by two repositories should live. The pinned fetch is the least-committal option, not an answer. STILL OPEN, and now filed as its own card.

Repository

targetRepo: motir-core — the repository the REMAINING deliverable landed in. motir-marketing's half shipped as #3 and #4 before this card was claimed; each delivered a proper subset, so neither is linked (a link asserts a pull request delivers the card, and the sync has no notion of partial completion). motir-core#2418 is the linked pull request that completes it.