embedded-react-sdk/CONTRIBUTING.md at main · Gusto/embedded-react-sdk · GitHub
Skip to content

Latest commit

 

History

History
465 lines (316 loc) · 24.4 KB

CONTRIBUTING.md

File metadata and controls

465 lines (316 loc) · 24.4 KB

@gusto/embedded-react-sdk - Internal README

This project is licensed under the Apache License 2.0. By submitting a contribution (for example, a pull request) for inclusion in this repository, you agree that your contribution is provided under the Apache License 2.0 — consistent with Section 5 of that license — and you represent that you have the right to submit it under those terms. A signed Contributor License Agreement (CLA) may be required for contributions going forward; we'll update this file if and when that process is in place.

To install dependencies:

npm install

To run:

npm run dev

Local development

For components

If you just need to develop a component outside of its context within a workflow, you can use Storybook:

npm run storybook

For workflows

If you need to test your component within the larger context of a workflow, you'll need to setup Zenpayroll + GWS-Flows.

  • Follow setup instructions for Zenpayroll and start local dev server

    • In another tab run the following commands in Zenpayroll directory(these generate necessary partner account for gws-flows) :
    bundle exec rails runner "DevAccountCreator.new.create_dev_accounts"
    rake partners_api:dev_setup_for_gws_onboarding
  • Follow setup instructions in the readme for GWS Flows

  • in SDK, Run npm run dev:setup; if that fails, follow the instructions below to setup manually:

    • Run npm link ../gws-flows/node_modules/react in sdk folder - this is needed to avoid duplicate react instances in local development due to using npm/yarn link
    • In GWS-Flows project folder, run yarn link -r ../embedded-react-sdk
  • In SDK, run npm run dev

Now your local changes appear in GWS Flows.

To see the SDK running in GWS Flows, visit it locally and choose React SDK (New company) or React SDK (Company Onboarded) under Select a Type and click Create Demo

Components and Flows will be shown at the top of the page in a nav. Company Components will automatically appear as they are added. Select a Flow or Component to view it

How to leverage Translations

Translations are stored in /src/i18n.

The are broken out by locale (e.g. en), then by namespace, which is usually the name of the component they are used in. The source of truth is the en locale - the rest will be autogenerated by translation service.

After writing the new translations, run npm run i18n:generate to regenerate the checked-in src/i18n/types.d.ts (the Translations namespace and Resources map that type i18next and back the browsable Translations reference). Commit the regenerated file alongside your JSON — CI's derive-check-i18n check fails if it is stale.

In your components, first use useI18n hook, which takes in component namespace and loads appropriate resource file into dictionary. After that use the useTranslation hook to access the translations in that component and any child components.

Translation Key Guidelines

Follow these conventions when creating or modifying translation keys:

Naming Convention

Use camelCase for all translation key names:

{
  "pageTitle": "Federal Tax Information",
  "federalEinLabel": "Federal EIN",
  "federalEinDescription": "Your company's Federal Employer Identification Number",
  "continueCta": "Continue"
}

Standard Suffixes

Use consistent suffixes to indicate the purpose of each key:

  • Cta - Call-to-action buttons (e.g., submitCta, continueCta, cancelCta)
  • Label - Form field labels (e.g., firstNameLabel, emailLabel)
  • Description - Help text or descriptions (e.g., federalEinDescription)
  • Title - Page or section headings (e.g., pageTitle, sectionTitle)
  • Placeholder - Input placeholders (e.g., emailPlaceholder)
  • Error - Error messages (e.g., requiredFieldError, invalidEmailError)

Grouping Related Keys

Group related keys using nested objects for better organization:

{
  "validations": {
    "firstName": "First name is required",
    "lastName": "Last name is required",
    "email": "Email address is required and must be valid",
    "address": {
      "street1": "Street address is required",
      "city": "City is required",
      "state": "State is required"
    }
  },
  "labels": {
    "openMenu": "Open menu",
    "menuLabel": "Menu"
  },
  "alerts": {
    "progressSaved": "Your progress has been saved",
    "errorEncountered": "There was a problem with your submission"
  }
}

Common grouping patterns:

  • validations - Form validation error messages
  • labels - Accessibility labels and field labels
  • alerts - Alert and notification messages
  • icons - Icon accessibility labels
  • table - Table-related labels (column headers, actions)

When to Use snake_case

Only use snake_case when required by external contracts:

  1. API Enum Values - When keys match backend API responses:
{
  "onboardingStatus": {
    "admin_onboarding_incomplete": "Admin-onboarding Incomplete",
    "self_onboarding_invited": "Self-onboarding: Invited"
  }
}
  1. i18next Pluralization - Required by i18next for plural forms:
{
  "priority_one": "{{count}}st",
  "priority_two": "{{count}}nd",
  "priority_few": "{{count}}rd",
  "priority_other": "{{count}}th"
}
  1. Programmatic Identifiers - When used as flow/step identifiers that match API:
{
  "stepTitles": {
    "add_addresses": "Add company addresses",
    "federal_tax_setup": "Company federal tax information"
  }
}

Examples

Good:

{
  "pageTitle": "Employee Profile",
  "continueCta": "Continue",
  "cancelCta": "Cancel",
  "validations": {
    "firstName": "First name is required",
    "emailFormat": "Email must be valid format"
  },
  "labels": {
    "personalInfo": "Personal Information"
  }
}

Avoid:

{
  "page_title": "Employee Profile", // ❌ Don't use snake_case
  "ContinueCTA": "Continue", // ❌ Don't capitalize suffix
  "validation_first_name": "First name...", // ❌ Don't use snake_case
  "first_name_validation": "First name...", // ❌ Poor grouping
  "lbl_personal_info": "Personal Info" // ❌ Don't abbreviate
}

Syncing translations with Lokalise

Source strings and reviewed translations flow to and from Lokalise via two workflows:

  • Push Sources — runs automatically on every push to main that touches src/i18n/en/**.json. Pushes the en source strings to Lokalise. No PR involved, so it's not affected by anything below.
  • Pull Translations — manual (workflow_dispatch only, run a few times a month as translations get reviewed in Lokalise). Downloads reviewed es_US strings and opens a PR (branch lokalise-update-<hash>, labeled lokalise-translation) via Gusto/embedded-i18n-actions/pull_translations.

Both workflows, plus check-for-manual-translations (below), live in Gusto/embedded-i18n-actions and this repo's .github/workflows/.

Merging a Pull Translations PR

The PR the workflow opens needs one manual step before it can merge — ci.yaml's required checks (build, format, lint, etc.) never run on it automatically. Root cause: ci.yaml only triggers on push/merge_group, and the PR's branch is pushed using the default GITHUB_TOKEN — GitHub's built-in anti-recursion rule means a GITHUB_TOKEN-authenticated push never triggers other push-triggered workflows. (We tried minting a GitHub App token instead to work around this — see #2680 — but the app available to this repo only has contents: write, not pull_requests: write, so PR creation itself failed outright. Reverted in #2706. Fixing this for real needs either that app's permissions extended or a dedicated PAT — both blocked on org/app-admin access at the time of writing. If you have that access, revisiting this is worthwhile so the step below can be removed.)

To merge a Pull Translations PR:

  1. Trigger the workflow: Actions tab → Pull Translations → Run workflow.

  2. Wait for the lokalise-update-<hash> PR to appear (labeled lokalise-translation).

  3. Check out the branch locally and push it yourself:

    git fetch origin
    git checkout lokalise-update-<hash>
    git merge origin/main   # or rebase — either works, see note below
    git push origin lokalise-update-<hash>

    A push made with your own credentials isn't subject to the anti-recursion rule, so it fires ci.yaml normally. If the branch is already up to date with main (git merge reports "Already up to date"), there's nothing to merge — push an empty commit instead so a push event still fires: git commit --allow-empty -m "chore: trigger CI" && git push.

  4. Check format. The Lokalise CLI export doesn't add a trailing newline to the downloaded JSON files, which Prettier requires — format will fail on any file Lokalise actually changed. Run npx prettier --write on the flagged files, commit, and push again:

    npx prettier <path-to-flagged-file> --write
    git add <path-to-flagged-file>
    git commit -m "style: add trailing newline to Lokalise-exported translation files"
    git push
  5. Once all required checks pass, review and merge as usual.

check-for-manual-translations

A separate check (.github/workflows/check-manual-translations.yaml) blocks manual hand-edits to src/i18n/es_US/** in any PR — those files should only ever be updated by the Pull Translations workflow. It skips validation when the lokalise-translation label is present (checked live via gh pr view, not the webhook event payload, which is stale by the time these bot-created PRs are evaluated — see #2674 if this check ever regresses). This check is already fixed and working; it's unrelated to the manual step above.

Creating components

Block components

Block components are focused, reusable components that serve specific functionality. They follow these patterns:

  • Single Purpose: Each component should generally handle a specific task (e.g., list, form, etc.)
  • Base Component: Uses BaseComponent for consistent behavior and error handling
  • Compound Pattern: Exposes subcomponents (Head, List, Actions) for flexibility and composition

Examples

Flow components

Flow components compose block components and other flow components together using state machines to manage transitions. They follow these patterns:

  • State Management: Uses the Flow component with a state machine to handle transitions
  • Component Composition: Can compose both block components (e.g., list → form) and other flow components
  • Naming: Suffix with "Flow" (e.g., DocumentSigner, Locations)

For example, EmployeeOnboardingFlow composes both block components (profile, taxes) and other flow components (document signer) to create a complete onboarding experience.

Examples

Testing locally

You can run the test suite locally with the following command:

npm run test

Accessibility testing

Every test that renders a component is automatically checked for accessibility violations — a global afterEach hook (src/test/setup.ts) runs axe against the rendered DOM before cleanup. You don't need to add anything to a new test to get this coverage, and CI fails on any violation (zero-tolerance threshold) since this runs inside the same test:ci job that already gates PRs.

The global check uses a relaxed rule set (see INTEGRATION_TEST_DISABLED_RULES in src/test/accessibility.ts) that ignores page-level rules like region and heading-order — an isolated component rendered outside a real page layout will always trip those, since it has no surrounding landmarks or document title to check against.

Components with dedicated accessibility test cases can still opt in to a stricter, narrower check via expectNoAxeViolations(container) (from src/test/accessibility.ts, available globally in tests) scoped to just that component's render output — this catches real issues like missing landmark roles or bad heading order that the relaxed global check intentionally skips. Keep these where they already exist; they're checking something the global gate doesn't.

If a violation is a genuine false positive rather than a real bug, disable only that specific rule with a comment explaining why (following the existing entries in DEFAULT_DISABLED_RULES/INTEGRATION_TEST_DISABLED_RULES) — don't reach for a broader escape hatch.

Pull requests

When creating a pull request, use the provided PR template (.github/PULL_REQUEST_TEMPLATE.md). Here are the guidelines:

PR title format

Use conventional commits format for your PR title. PR titles are validated by CI. When a release is prepared with npm run release, commit history (which uses squash-merge PR titles) determines the version bump and populates the changelog — so PR title format directly affects how releases are documented.

Note: Commitlint runs in two places: as a pre-commit hook (via husky) validating individual commit messages, and as a CI check validating the PR title, which becomes the squash merge commit message.

Semantic versioning (semver) mapping

PR titles determine the version bump and changelog entry when a release is prepared, following semver.org specification:

During 0.x.x (pre-1.0 development):

Commit Type Version Bump When to Use
feat MINOR (0.1.0 → 0.2.0) New features or functionality
fix PATCH (0.1.0 → 0.1.1) Bug fixes
feat! or fix! (with !) MINOR (0.1.0 → 0.2.0) Breaking changes*
docs, chore, refactor, test, ci, style, perf, build, revert No bump Non-functional changes

*Per semver spec, during 0.x.x the API is considered unstable, so breaking changes bump MINOR instead of MAJOR. The jump to 1.0.0 will be an intentional decision when we're ready to declare a stable API.

Title examples

  • feat: add new component - New feature → MINOR bump
  • fix: resolve issue with form validation - Bug fix → PATCH bump
  • feat!: redesign JSX component props - Breaking change → MINOR bump (during 0.x.x)
  • chore: update dependencies - Maintenance → no version bump
  • refactor: simplify state machine logic - Code refactoring → no version bump
  • docs: update README - Documentation → no version bump

For work tied to a Jira ticket, include the ticket in the scope so the title still follows conventional commits:

  • feat(SDK-123): add payroll blocker alerts - MINOR bump
  • fix(SDK-456): correct date formatting - PATCH bump

PR description sections

  • Summary: Brief description of what the PR does and why
  • Changes: Briefly list only the most important changes (high-level only; do not enumerate every file-level change)
  • Demo: Screenshots or screen recordings showing the changes (when applicable)
  • Related: Links to Jira tickets, Figma designs, or related PRs
  • Testing: Instructions for reviewers to test the changes

Best practices

  • Keep PRs focused on a single concern when possible
  • Include visual demos (screenshots/videos) for UI changes
  • Link to relevant Jira tickets using the format [SDK-XXX](https://gustohq.atlassian.net/browse/SDK-XXX)
  • Add Storybook stories for new components
  • Include unit tests for new functionality
  • Run npm run test and npm run lint before submitting

Commits

All commits must follow commitlint enforced format: type(scope?): subject #scope is optional; multiple scopes are supported (current delimiter options: "/", "\" and ",") Following types are currently defined:

'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test'

Read more about conventional commits here

Documentation

You can find documentation on building with the Gusto Embedded React SDK in the docs directory within this repo.

Cutting a new release

Releases are prepared with release-it, which reads commits since the last release, proposes a semver bump, updates package.json and CHANGELOG.md, and creates a release branch ready for review.

Option 1: locally

From a clean main branch:

npm run release

release-it will:

  1. Show you the commits since the last release grouped by type
  2. Propose a version based on conventional commit prefixes (feat → MINOR, fix → PATCH, feat!/fix! → MINOR during 0.x.x)
  3. Ask you to confirm or override the target version
  4. Bump package.json, update package-lock.json, and prepend a new section to CHANGELOG.md
  5. Commit the changes and check out a chore/release-<version> branch automatically

Then push and open a PR:

git push -u origin chore/release-<version>
gh pr create --title "chore: release <version>"

Option 2: via GitHub Actions

Trigger the Prepare Release workflow from the Actions UI (Run workflow). It accepts an optional version override; if left blank it auto-detects from commits. The workflow creates the branch, commits the changes, and opens a PR automatically.

Stable releases

PRs that change the root package.json version are checked by release-readiness, regardless of branch name or PR title. Update the branch with the latest main, refresh the changelog, then get approval from another maintainer on the final commit. Any subsequent commit, including a merge from main, requires fresh approval. PRs without a version change pass this check without additional review requirements.

The check updates independently of the build when a PR or review changes, or when main advances. Merge queue checks use approvals of the original PR commits. To refresh the result manually, run Release Readiness from main.

After a release PR merges to main with a chore: release commit and CI passes, Publish to NPM starts automatically. The workflow uses npm-release to publish to npm, create a Git tag and GitHub Release with changelog notes, and send the release notification.

The intended configuration for npm-release allows workflows from main without a separate publishing approval once release-readiness is enforced. During rollout, keep the existing publishing approval until the new check is verified and required.

One-off builds

Use Publish RC to NPM for a one-off build from a branch or commit. Run the workflow from main and choose the source using its inputs. It publishes under rc, leaving latest unchanged, with no Git tag or GitHub Release.

When finished, use Retire RC, supplying the version currently tagged rc. It removes the tag; exact-version installs remain available. Both flows support dry_run; retirement defaults to preview, so set dry_run=false to remove the tag.

Both RC publishing and retirement use npm-release-manual and wait for another member of the Embedded SDK team to approve through the workflow run's Review deployments prompt. The release owner arranges this approval. Before approving, check the selected source commit and version in the run summary, or the expected version for retirement.

Manual stable release

Publish to NPM (Manual) is the escape hatch for when the automatic Publish to NPM path doesn't fire even though the release itself is fine — e.g. a release commit's CI run got cancelled by merge-queue churn, or failed on a flaky/unrelated job. It is not a way to bypass a real CI failure.

Run it from main, supplying the exact commit SHA of the chore: release commit to publish. It refuses anything that isn't an already-merged chore: release commit on main, and like automatic publishing, no-ops if that version is already on npm. It shares the npm-release-manual environment with RC publishing/retirement, so it needs the same Embedded SDK team approval through Review deployments — check the commit, message, and version in the run summary before approving.

Retiring a stable version

Stable versions should normally remain available. If one needs to be retired, ask an npm maintainer to deprecate the version and coordinate any GitHub Release cleanup.

Removing a harmful version

If a version accidentally contains secrets or malicious content, ask an npm maintainer to fully unpublish that version, subject to npm's unpublish policy. RC retirement leaves the package downloadable and is insufficient for this case. Rotate any exposed credentials and coordinate GitHub Release cleanup.

Testing release workflows

Publishing helpers live in .github/scripts/; each helper documents its invocation. Run the publishing helper tests locally with:

bash .github/scripts/publish-helpers.test.sh

These tests exercise inline publishing commands and helpers using mocked npm and HTTP responses plus local Git repositories, without publishing or sending notifications. CI also runs input validation, workflow guards, GitHub Release creation tests, and RC retirement tests. Live workflow runs are still needed to verify npm trust, environment approvals, and runner configuration.

Run release readiness tests without installing dependencies:

node --test .github/scripts/release-readiness.test.cjs

The event workflow is read-only; the evaluator runs trusted code from main and reads candidate files through the GitHub API without executing them.

To roll out the approval changes:

  1. Configure npm-release-manual to require another Embedded SDK team member's approval and prevent self-approval. All three environments must permit workflows from main only; RC and manual-publish sources are selected through workflow inputs. Update the npm trusted publisher configuration for RC publishing, retirement, and manual stable publishing to match npm-release-manual, then verify all three workflows' OIDC and approval behavior.
  2. After the readiness workflows land on main, verify PR push, review approval/dismissal, main advancement, and merge queue events. Add release-readiness as a required check on main, selecting GitHub Actions as its source.
  3. Only after the check is verified and required, remove the stable publishing approval from npm-release. Keep approval required in npm-release-manual.

Markdown links

Inside docs/, use relative links between files (e.g. [link](../getting-started/index.md)), not /docs/... absolute links. Absolute links don't resolve correctly when the same file is served at a versioned URL like /docs/0.47/....

<!-- Good -->

See the [Getting Started guide](../getting-started/index.md).

<!-- Bad — breaks when served at /docs/0.47/... -->

See the [Getting Started guide](/docs/getting-started/index.md).