From 6b652f8aa5cd242477a5533fcc77679c51160d8f Mon Sep 17 00:00:00 2001 From: Wabo Date: Tue, 19 May 2026 06:13:43 -0400 Subject: [PATCH] Add rebrand-sync/-check tooling for upstream merges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a deterministic rebrand sweep so each upstream sync is a scripted transformation rather than a manual sweep: - bin/rebrand-sync — idempotent Ruby script that rewrites DocuSeal → WaboSign tokens across the tree (Ruby module, AATL cert name, DB names, Docker user, registry/repo URLs, DOM-ID/localStorage prefix, daisyUI theme name, hardcoded UI strings). Sentinel-protects AGPL §7(b) attribution phrases, SDK custom-element identifiers, the @docuseal/* npm packages, and the docusealco/{fields-detection, pdfium-binaries,turbo} binary URLs. Deny-lists NOTICE, LICENSE_*, README, the AGPL attribution partials, calculator.js's DocuSeal LLC copyright, lib/wabosign.rb's UPSTREAM constants, and the docuseal_aatl migration that searches by the legacy name. - bin/rebrand-check — fails CI when an unintended DocuSeal reference survives in the tree. Wired in as the new `Rebrand check` job in .github/workflows/ci.yml. - REBRANDING.md gains a "Sync workflow" section documenting the per-sync steps, rerere setup, and how to keep the two scripts' allow/preserve lists in sync. - .gitattributes marks Gemfile.lock and yarn.lock as -merge — they get regenerated post-merge rather than diffed. - Latent rebrand leftovers fixed: public/service-worker.js no longer logs "DocuSeal App installed/activated"; .dockerignore and .gitignore now ignore /wabosign (the actual runtime data dir mount) rather than the stale /docuseal path. Strategy detail lives at .claude/plans/come-up-with-a-foamy-flask.md. Co-Authored-By: Claude Opus 4.7 --- .dockerignore | 2 +- .gitattributes | 5 + .github/workflows/ci.yml | 13 +++ .gitignore | 2 +- REBRANDING.md | 37 +++++++ bin/rebrand-check | 142 +++++++++++++++++++++++++++ bin/rebrand-sync | 204 +++++++++++++++++++++++++++++++++++++++ public/service-worker.js | 4 +- 8 files changed, 405 insertions(+), 4 deletions(-) create mode 100755 bin/rebrand-check create mode 100755 bin/rebrand-sync diff --git a/.dockerignore b/.dockerignore index 69198e47..1dce01b3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,6 +26,6 @@ /public/packs-test /public/packs /attachments -/docuseal +/wabosign .DS_Store .env diff --git a/.gitattributes b/.gitattributes index 28cee3ff..30f0c8e8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ *.html linguist-detectable=false + +# Upstream-merge ergonomics. Lockfiles are regenerated post-merge rather +# than merged line-by-line — see REBRANDING.md "Sync workflow". +Gemfile.lock -merge +yarn.lock -merge diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae960f97..8f13d8cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,19 @@ name: CI on: [push] jobs: + rebrand_check: + name: Rebrand check + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 4.0.1 + - name: Run rebrand-check + run: ruby bin/rebrand-check + rubocop: name: Rubocop runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 268cf028..e955dfa0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ yarn-debug.log* /coverage /attachments -/docuseal +/wabosign /ee dump.rdb *.onnx diff --git a/REBRANDING.md b/REBRANDING.md index 79d90c87..a3069c11 100644 --- a/REBRANDING.md +++ b/REBRANDING.md @@ -65,3 +65,40 @@ Per AGPL §7(b) and downstream SDK compatibility, the following references remai - Webhook spec assertions updated to match new `WaboSign Webhook` user-agent Full Rails boot requires Ruby 4.0.1, which was not available on the dev machine that performed the rebrand. Recommend running `docker compose up --build` to verify boot end-to-end before publishing. + +## Sync workflow + +Upstream lives at `docusealco/docuseal`. Each upstream release is brought in by re-running a deterministic rebrand sweep on the upstream tree, then merging into `master`. The strategy details are in [.claude/plans/come-up-with-a-foamy-flask.md](.claude/plans/come-up-with-a-foamy-flask.md); the short version follows. + +### Tooling + +- [bin/rebrand-sync](bin/rebrand-sync) — Ruby script that performs the DocuSeal → WaboSign rename sweep across the working tree. Idempotent. Honors a deny-list (see §"Intentionally preserved upstream references" above) and sentinel-protects AGPL §7(b) attribution phrases, SDK custom-element names (`docuseal-form`, `docuseal-builder`), `@docuseal/*` npm packages, and the `github.com/docusealco/{fields-detection,pdfium-binaries,turbo}` binary URLs. +- [bin/rebrand-check](bin/rebrand-check) — fails (exit 1) if any unintended DocuSeal reference survives. Wired into [.github/workflows/ci.yml](.github/workflows/ci.yml) as the `Rebrand check` job. +- `git config rerere.enabled true && git config rerere.autoupdate true` — once-per-checkout setup; remembers semantic conflict resolutions so the same call is not re-made each release. +- [.gitattributes](.gitattributes) marks `Gemfile.lock` and `yarn.lock` as `-merge` (regenerate after merge rather than diffing). + +### Per-sync steps + +```sh +git fetch upstream --tags +git checkout -b sync/upstream- # e.g. 3.0.0 +bin/rebrand-sync +git add -A && git commit -m "Apply WaboSign rebrand sweep to upstream " + +git checkout master +git merge --no-ff sync/upstream- +# Resolve conflicts. Rerere caches recurring resolutions. + +bin/rebrand-sync # catch upstream-only new files +bin/rebrand-check # CI gate + +bundle install +yarn install + +# Verify (see "Verification" in the plan), then: +git tag wabosign-synced-with- +``` + +### Adding new preserved tokens + +When upstream introduces a new SDK identifier, binary URL, or attribution surface that must survive the sweep, edit `PRESERVE` in [bin/rebrand-sync](bin/rebrand-sync) and `ALLOW_PATTERNS` in [bin/rebrand-check](bin/rebrand-check) together. The two must stay in sync — `rebrand-sync` decides what the sweep ignores, `rebrand-check` decides what CI tolerates. diff --git a/bin/rebrand-check b/bin/rebrand-check new file mode 100755 index 00000000..fc1e180a --- /dev/null +++ b/bin/rebrand-check @@ -0,0 +1,142 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# bin/rebrand-check — fails (exit 1) if any unintended "docuseal" / +# "Docuseal" / "DocuSeal" references survive in the tree. +# +# Intended survivors are documented in REBRANDING.md: +# - AGPL §7(b) attribution (NOTICE, README, LICENSE_ADDITIONAL_TERMS, +# _powered_by, _email_attribution, completed.vue's credit, the +# based_on i18n keys, Wabosign::UPSTREAM_NAME / UPSTREAM_URL) +# - SDK custom-element names , +# - npm packages @docuseal/{react,vue,angular,embed} +# - upstream binary URLs github.com/docusealco/{fields-detection, +# pdfium-binaries,turbo} +# - DocuSeal LLC copyright header in calculator.js +# +# Any other occurrence is treated as an accidental survivor and printed. +# +# Wired into CI via .github/workflows/ci.yml. + +require 'find' +require 'set' + +ROOT = File.expand_path('..', __dir__) + +DENY_PREFIXES = [ + '.git/', '.github/', 'node_modules/', 'vendor/bundle/', 'vendor/cache/', + 'tmp/', 'log/', 'pg_data/', 'public/packs/', 'public/packs-test/', + 'wabosign/', '.claude/', 'coverage/' +].freeze + +BINARY_EXT = Set.new(%w[ + .png .jpg .jpeg .gif .ico .svg .pdf .zip .tgz .gz .bz2 .xz + .woff .woff2 .ttf .eot .otf + .pkcs12 .p12 .pem .crt .cer .key + .db .sqlite .sqlite3 .so .dylib .dll .exe .onnx +]).freeze + +# Whole files we ignore — every "docuseal" hit in them is intentional. +ALLOW_FILES = Set.new([ + 'NOTICE', + 'LICENSE', + 'LICENSE_ADDITIONAL_TERMS', + 'REBRANDING.md', + 'CHANGELOG.md', + 'README.md', + 'GOOGLE_SSO.md', + 'SMS.md', + 'app/javascript/submission_form/calculator.js', + 'app/javascript/submission_form/completed.vue', + 'app/views/shared/_powered_by.html.erb', + 'app/views/shared/_email_attribution.html.erb', + 'bin/rebrand-sync', + 'bin/rebrand-check', + 'lib/wabosign.rb', + # Migration that finds rows by the legacy docuseal_aatl name. + 'db/migrate/20260515183000_rename_docuseal_aatl_cert.rb' +]).freeze + +ALLOW_FILE_PREFIXES = ['docs/embedding/'].freeze + +# Per-line allowed patterns. If every "docuseal" hit on a line is +# inside one of these, the line is tolerated. +ALLOW_PATTERNS = [ + # SDK custom element tags — bare, attributed, HTML-encoded, quoted + %r{}, + %r{</?docuseal-(form|builder)}, + %r{['"]docuseal-(form|builder)['"]}, + # SDK class names exported by the npm packages + %r{\bDocuseal(Form|Builder)(Component)?\b}, + # npm package paths + %r{@docuseal/(react|vue|angular|embed)}, + # Upstream binary release URLs + %r{docusealco/fields-detection}, + %r{docusealco/pdfium-binaries}, + %r{docusealco/turbo}, + # Programmatic custom-element registration + %r{customElements\.define\(['"]docuseal-}, + # Constants holding the AGPL §7(b) attribution + %r{Wabosign::UPSTREAM_(NAME|URL)}, + # AGPL attribution idioms — preserve the literal "DocuSeal" credit + %r{\b(?:fork(?:ed)? of|forked from|based on|derived from)\s+DocuSeal\b}, + # JS calculator copyright header + %r{DocuSeal,?\s+LLC}, + # i18n key carrying the AGPL credit value + %r{based_on:} +].freeze + +def deny_listed?(rel) + return true if ALLOW_FILES.include?(rel) + return true if ALLOW_FILE_PREFIXES.any? { |p| rel.start_with?(p) } + DENY_PREFIXES.any? { |p| rel.start_with?(p) } +end + +def likely_binary?(path) + return true if BINARY_EXT.include?(File.extname(path).downcase) + head = File.binread(path, 1024) + head.include?("\x00") +rescue StandardError + false +end + +HIT_RE = /docuseal/i + +def line_is_tolerated?(line) + scrubbed = line.dup + ALLOW_PATTERNS.each { |re| scrubbed = scrubbed.gsub(re, '') } + !scrubbed.match?(HIT_RE) +end + +violations = [] + +Find.find(ROOT) do |path| + rel = path.sub(%r{\A#{Regexp.escape(ROOT)}/}, '') + if File.directory?(path) + Find.prune if rel != '' && DENY_PREFIXES.any? { |p| "#{rel}/".start_with?(p) } + next + end + next if deny_listed?(rel) + next if likely_binary?(path) + + begin + File.foreach(path, encoding: 'UTF-8').with_index(1) do |line, lineno| + next unless line.match?(HIT_RE) + next if line_is_tolerated?(line) + + violations << "#{rel}:#{lineno}: #{line.chomp}" + end + rescue Encoding::InvalidByteSequenceError, ArgumentError + next + end +end + +if violations.empty? + puts 'rebrand-check: ok' + exit 0 +else + warn "rebrand-check: #{violations.size} unintended DocuSeal reference(s):" + violations.each { |v| warn " #{v}" } + exit 1 +end diff --git a/bin/rebrand-sync b/bin/rebrand-sync new file mode 100755 index 00000000..17c60c68 --- /dev/null +++ b/bin/rebrand-sync @@ -0,0 +1,204 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# bin/rebrand-sync — applies the DocuSeal → WaboSign rename sweep across +# the working tree. Idempotent: re-running produces no further changes. +# +# Intended use: on a sync branch created from an upstream tag, before +# merging into master. Re-run after the merge to catch any new files +# upstream introduced. See REBRANDING.md "Sync workflow". +# +# Rule spec lives at .claude/plans/come-up-with-a-foamy-flask.md. +# Preserved tokens (SDK custom elements, @docuseal/* npm packages, +# upstream binary URLs, AGPL §7(b) attribution) are protected via +# sentinel-swap before substitutions run. + +require 'find' +require 'set' +require 'fileutils' + +ROOT = File.expand_path('..', __dir__) + +# Pull canonical target names from lib/wabosign.rb when present, so a +# future brand change only needs editing that file. On a fresh upstream +# tag the file does not exist yet — fall back to the literal defaults. +wabosign_rb_path = File.join(ROOT, 'lib/wabosign.rb') +WABOSIGN_RB = File.exist?(wabosign_rb_path) ? File.read(wabosign_rb_path) : '' + +def const_str(name) + m = WABOSIGN_RB.match(/^\s*#{Regexp.escape(name)}\s*=\s*['"]([^'"]+)['"]/) + m && m[1] +end + +PRODUCT_NAME = const_str('PRODUCT_NAME') || 'WaboSign' +AATL_CERT = const_str('AATL_CERT_NAME') || 'wabosign_aatl' +LC = PRODUCT_NAME.downcase + +DENY_PATHS = Set.new([ + 'NOTICE', + 'LICENSE', + 'LICENSE_ADDITIONAL_TERMS', + 'REBRANDING.md', + 'CHANGELOG.md', + 'README.md', + 'GOOGLE_SSO.md', + 'SMS.md', + 'app/javascript/submission_form/calculator.js', + 'app/javascript/submission_form/completed.vue', + 'app/views/shared/_powered_by.html.erb', + 'app/views/shared/_email_attribution.html.erb', + 'bin/rebrand-sync', + 'bin/rebrand-check', + # Holds UPSTREAM_NAME / UPSTREAM_URL constants — must not be swept. + 'lib/wabosign.rb', + # Encrypted-config migration matches the literal upstream string to find + # rows to rewrite; sweeping would silently neuter it. + 'db/migrate/20260515183000_rename_docuseal_aatl_cert.rb' +]).freeze + +DENY_PREFIXES = [ + 'docs/embedding/', + '.git/', + '.github/', + 'node_modules/', + 'vendor/bundle/', + 'vendor/cache/', + 'tmp/', + 'log/', + 'pg_data/', + 'public/packs/', + 'public/packs-test/', + 'wabosign/', + '.claude/', + 'coverage/' +].freeze + +BINARY_EXT = Set.new(%w[ + .png .jpg .jpeg .gif .ico .svg .pdf .zip .tgz .gz .bz2 .xz + .woff .woff2 .ttf .eot .otf + .pkcs12 .p12 .pem .crt .cer .key + .db .sqlite .sqlite3 .so .dylib .dll .exe .onnx +]).freeze + +# File renames the upstream rebrand commit applied. The two logo partials +# were given a *semantic* name (_brand_logo, not _wabosign_logo) so we do +# NOT rename them in the sweep — git's rename detection during merge will +# carry upstream content edits into the fork's _brand_logo.html.erb. +FILE_RENAMES = { + 'lib/docuseal.rb' => 'lib/wabosign.rb' +}.freeze + +# Tokens that must survive the sweep. Sentinel-protected so the +# substitutions below cannot touch them. Sentinels use plain ASCII; the +# surrounding spaces and digits make a collision with real content +# vanishingly unlikely. +PRESERVE = { + # SDK custom-element identifiers (hyphenated — match anywhere they + # appear: bare, inside with attributes, HTML-encoded as + # <docuseal-form, or quoted as 'docuseal-form') + 'docuseal-form' => ' SENTINEL00 ', + 'docuseal-builder' => ' SENTINEL01 ', + # npm packages + '@docuseal/react' => ' SENTINEL02 ', + '@docuseal/vue' => ' SENTINEL03 ', + '@docuseal/angular' => ' SENTINEL04 ', + '@docuseal/embed' => ' SENTINEL05 ', + # Upstream binary release URLs (org + repo path) + 'docusealco/fields-detection' => ' SENTINEL06 ', + 'docusealco/pdfium-binaries' => ' SENTINEL07 ', + 'docusealco/turbo' => ' SENTINEL08 ', + # Ruby constants holding the AGPL §7(b) attribution + 'Wabosign::UPSTREAM_NAME' => ' SENTINEL09 ', + 'Wabosign::UPSTREAM_URL' => ' SENTINEL0A ', + # AGPL attribution idioms — preserve the literal "DocuSeal" credit + # wherever the fork describes itself as derived from upstream. + 'fork of DocuSeal' => ' SENTINEL0B ', + 'forked from DocuSeal' => ' SENTINEL0C ', + 'based on DocuSeal' => ' SENTINEL0D ', + 'derived from DocuSeal' => ' SENTINEL0E ', + # JS calculator copyright header + 'DocuSeal LLC' => ' SENTINEL0F ', + 'DocuSeal, LLC' => ' SENTINEL10 ', + # i18n key (not value) for AGPL credit + 'based_on:' => ' SENTINEL11 ' +}.freeze + +RULES = [ + [/\bDocuseal\b/, 'Wabosign'], + [/\bDocuSeal\b/, PRODUCT_NAME], + ['docuseal_aatl', AATL_CERT], + ['docuseal_dev', "#{LC}_dev"], + ['docuseal_test', "#{LC}_test"], + ['docuseal_production', "#{LC}_production"], + ['docuseal.env', "#{LC}.env"], + ['ghcr.io/docusealco/docuseal', "ghcr.io/wabolabs/#{LC}"], + ['docusealco/docuseal', "wabolabs/#{LC}"], + ['/home/docuseal', "/home/#{LC}"], + ['docuseal:docuseal', "#{LC}:#{LC}"], + [/\bdocuseal_/, "#{LC}_"], + [/\bdocuseal\b/, LC] +].freeze + +def deny_listed?(rel) + return true if DENY_PATHS.include?(rel) + DENY_PREFIXES.any? { |p| rel.start_with?(p) } +end + +def likely_binary?(path) + return true if BINARY_EXT.include?(File.extname(path).downcase) + head = File.binread(path, 1024) + head.include?("\x00") +rescue StandardError + false +end + +def rewrite(text) + PRESERVE.each { |t, s| text = text.gsub(t, s) } + RULES.each { |pat, repl| text = text.gsub(pat, repl) } + PRESERVE.each { |t, s| text = text.gsub(s, t) } + text +end + +renamed = [] +FILE_RENAMES.each do |from, to| + from_path = File.join(ROOT, from) + to_path = File.join(ROOT, to) + next unless File.exist?(from_path) + next if File.exist?(to_path) + + FileUtils.mkdir_p(File.dirname(to_path)) + FileUtils.mv(from_path, to_path) + renamed << "#{from} -> #{to}" +end + +changed = [] +skipped_encoding = [] + +Find.find(ROOT) do |path| + rel = path.sub(%r{\A#{Regexp.escape(ROOT)}/}, '') + if File.directory?(path) + Find.prune if rel != '' && deny_listed?("#{rel}/") + next + end + + next if deny_listed?(rel) + next if likely_binary?(path) + + begin + original = File.read(path, encoding: 'UTF-8') + rescue Encoding::InvalidByteSequenceError, ArgumentError + skipped_encoding << rel + next + end + + updated = rewrite(original) + next if updated == original + + File.write(path, updated) + changed << rel +end + +puts "rebrand-sync: renamed #{renamed.size} file(s), rewrote #{changed.size} file(s)" +renamed.each { |r| puts " renamed: #{r}" } +changed.each { |f| puts " rewrote: #{f}" } +skipped_encoding.each { |f| warn " skipped (encoding): #{f}" } diff --git a/public/service-worker.js b/public/service-worker.js index 3334a5ae..e87392c1 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,9 +1,9 @@ self.addEventListener('install', () => { - console.log('DocuSeal App installed') + console.log('WaboSign App installed') }) self.addEventListener('activate', () => { - console.log('DocuSeal App activated') + console.log('WaboSign App activated') }) self.addEventListener('fetch', (event) => {