release(sofra): partner brand (S1 + S3a) — publishes images, does NOT deploy - #210
Merged
Merged
Conversation
…et (S1) (#204) * feat(partner): record a partner's public brand, published to nobody yet (S1) A PARTNER can enter and edit the public name and contact details their resold restaurants could be credited to, and the founder can read them on /admin/partners/[id]. Nothing is published. No tenant site reads PartnerBrand; publishToTenants defaults to false, is read by nothing, and the checkbox ships disabled with copy saying so — the publishing half is gated on the owner decision in SOFRA-PARTNER-PLAN 11e. The record is deliberately separate from BillingIdentity, which is the private legal and tax record: for a sole trader its legalName is a natural person and its address is a home address. prefillFromBillingIdentity carries across the trade name and nothing else; renderableBrand() is the single choke point that returns null for an unpublished brand. * chore(partner): take the two SonarCloud smells on the brand slice S7754: .filter(Boolean).length > 0 becomes a joined string tested for truthiness, which the address line beside it already did. S6582: !brand || !brand.publishToTenants becomes !brand?.publishToTenants — same three cases (null, undefined, opted out), one expression.
… refuse their legal name (S3a) (#205) * feat(partner): publish a partner's brand into the registry entry, and refuse their legal name (S3a) `renderableBrand` is tightened to the D-B1 projection — `displayName` plus, when there is one, `websiteUrl`, and nothing else — and now takes the partner's LEGAL name as well, because D-B1a is a rule about the relationship between the two records: a display name that IS the legal name is refused, compared normalised (trim, case-fold, collapse whitespace). `prefillFromBillingIdentity` offers `tradeName ?? legalName`, so a sole trader who saves the form untouched would otherwise have their personal name published on a restaurant's public page by a convenience nobody decided on. Dropping tagline/email/phone/address/postcode/city/country from the projection is a TIGHTENING, so it is self-announcing: the unit assertions that named those fields went red and were rewritten deliberately. Widening back to a contact block is the silent direction and needs its own controls plus a pii-inventory row. `buildTenantRegistryEntry` emits `partner_name:` and `partner_url:` as flat keys (§11d2 — provision-tenant.sh flattens each value with `str(v)`, so a nested map would arrive in the shell as a stringified Python dict). It never emits `partner_attribution:`: absent means true (D-B2), so an always-present key would be a no-op line on every entry — it is the restaurant's switch, hand-added on their behalf. ABSENCE IS THE CONTRACT, exactly as `base_domain` documents it, and the proof is that every pre-existing test of that function passes unchanged. The input field is typed as `renderableBrand`'s OUTPUT, so the choke point is the only way a name can reach an entry, and it is filled in `openProvisioningPr` rather than by each caller: the credit is derived from the slug — nobody types it — so resolving it where the entry is written means no caller can forget it and none can inject one. The lookup matches the slug through `Client.tenantSlug` OR the reseller's plan, because the former is set by an admin only AFTER provisioning; it fails open to no credit. The form now says why, before the save: a billing record with no trade name gets a note, and a display name echoing the legal name is called out as not publishable while the save still succeeds — it is their record; what is refused is publishing it. The provisioning PR body says when an entry carries a credit, because that PR is the founder's review checkpoint (ADR-012) and it is the moment someone should notice a name is about to become public. Two files were split at the 200-LOC limit rather than baselined (partner-brand-publish.ts, provisioning-pr-blocks.ts); both are in the coverage floor's include list, so the split moved no code out of scope. Plan: SOFRA-PARTNER-PLAN §11e, slice S3a. * fix(a11y): use <output> for the not-published notice, not role=status (S6819) SonarCloud's rule, and it is right: the native element carries the same live region and is announced by assistive tech that does not implement the ARIA role. The region itself is deliberate — the partner types and the verdict changes under them, so it has to be spoken rather than merely rendered.
Release PR #210 is refused by the `main-develop` ruleset's strict up-to-date rule: develop's tip does not contain main's tip. That is a commit-graph fact, not a content one — every release merges develop→main as a merge commit, so develop is BEHIND the moment a release lands and stays there until it is back-merged. Content-tested first, because "BEHIND" and "main holds something develop lacks" are different claims: * no main-only files at all (`git diff --diff-filter=A` is empty) * `git merge-tree --write-tree` CLEAN * the only lines the diff attributes to main are develop's own refactors — main calls `buildTenantRegistryEntry(input)` where develop calls `buildTenantRegistryEntry({ ...input, partnerBrand })`, a strict superset, and the `"domains"` message key is present in all six locales on BOTH sides So this merge exists to record ancestry and nothing else. The resulting tree is identical to develop's, which is asserted below rather than assumed. Merged as a real merge commit: a squash would keep the (empty) content change and drop the ancestry link, leaving the next release to hit the same refusal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`develop` has been red at 48091a5 — the commit this branch was cut from — with three jobs failing on one cause. Trivy is named "misconfig + secrets" but was reporting dependency CVEs, so it, OSV-Scanner and `npm audit (high+)` were all naming the same packages: browserslist 4.28.4 CVE-2026-73088/73089 prototype pollution + unbounded memory growth -> 4.28.7 mysql2 3.15.3 GHSA-3f6p-5ww8-9rcr auth-plugin downgrade leaks plaintext credentials -> 3.22.0 fast-uri 3.1.5 4 advisories SSRF + host confusion -> 3.1.6 `fast-uri` already had an override, at `^3.1.4`, which happily resolved to the vulnerable 3.1.5 — a pin that exists is not the same as a pin that covers the advisory. `mysql2` arrives through `prisma@7.8.0`, whose CLI bundles a driver for every database it supports. Sofra is Postgres and never executes it, but the merge gate counts findings rather than judging them, and an unexecuted credential-leak path is still a credential-leak path if the dependency graph ever shifts. Fixed with `overrides` rather than direct dependencies: none of the three is a direct dep, so declaring one would risk EOVERRIDE on any future spec mismatch. Resolved to browserslist 4.28.8, mysql2 3.24.3, fast-uri 3.1.7 — `npm audit` now reports 0 vulnerabilities. Lockfile reconciled with `npx npm@10 install --package-lock-only` against the existing lock rather than regenerated from scratch, so the linux-x64 optional binaries CI needs survive: 58 before, 58 after. Verified with `npx npm@10 ci --dry-run`, the only local command that runs what CI runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chore: back-merge main into develop, and clear three high advisories reddening CI
|
mahmutkaya
added a commit
that referenced
this pull request
Sep 2, 2026
chore: back-merge the #210 release into develop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What ships
develop→main, two PRs — the partner-brand slices, which the ROADMAP has been tracking as outstanding item #4 ("Sofra is 2 commits ahead ofmain").1730 insertions across the Prisma schema, four locale bundles, unit and e2e tests.
Read this before rolling it
Merging here does not deploy.
build-image.ymlpublishes:latestand:migrateonmain; the control plane is then rolled by hand on the box, which is the step that applies the migration.That matters because this release carries
prisma/migrations/20260830120000_partner_brand— a schema change against the control-plane database, which holds partner, billing and CRM records. So the sequence is: this merge publishes the images; someone rolls when they choose to, and the migration lands then.I did not write or test this feature. It is released here because it is on the ROADMAP's own "Now" list as an outstanding release and because the merge itself deploys nothing. The CI on this PR — including its unit and e2e suites — is the evidence behind it, not a review by me. Take the hand-roll as a separate decision.
Blast radius of the merge itself
Images published to GHCR.
sofrapiwas.comcontinues to serve663fba9until someone rolls it.🤖 Generated with Claude Code