1. Branch Naming
Choose the prefix that matches your work:
| Type | Pattern | Example |
|---|---|---|
| Feature (ClickUp, default) | feature/<TASK-ID>_Short-Description |
feature/DEV-627_Forum-Update |
| Docs (ClickUp) | docs/<TASK-ID>_Short-Description |
docs/DEV-458_Clarify-undo-redo-docs |
| Feature (public GitHub issue) | feature/issue-xxxx |
feature/issue-11832 |
| Docs (public GitHub issue) | docs/issue-xxxx |
docs/issue-9500 |
| Release | release/x.y.z |
release/16.1.0 |
<TASK-ID> is the ClickUp custom ID. Its prefix follows the space the task lives in, so it is not always DEV: docs/SU-833_BeforeKeyDown-Return-False-Note and feature/PRO-858_Theme-API-e2e-test-data-driven-for-each-theme are both valid. Copy the prefix from the task, never assume one.
When working from a ClickUp task, the human-readable custom ID (e.g. DEV-627, IT-42) must appear in the branch name so ClickUp links automatically. Never use the internal ClickUp hash ID (e.g. 86c9j4fxj) — it is not a valid task identifier for branch linking.
Important: clickup_create_task returns custom_id: null in its response. Always call clickup_get_task immediately after creating a task to retrieve the real custom ID before naming the branch:
1. clickup_create_task → returns task_id (hash, e.g. "86c9j4fxj")
2. clickup_get_task(task_id) → returns custom_id (e.g. "DEV-1532")
3. Use custom_id in the branch name: feature/DEV-1532_Short-Description
2. Pre-flight Checks
Run these before opening the PR. Fix any failures first.
# Lint
npm run eslint --prefix handsontable
npm run stylelint --prefix handsontable
# Build (wrappers depend on this output)
npm run build --prefix handsontable
# Unit tests for the area you changed
npm run test:unit --prefix handsontable --testPathPattern=<regex>
# E2E tests for the area you changed
npm run test:e2e --prefix handsontable --testPathPattern=<regex>
# If you touched a wrapper, test it too
npm run test --prefix wrappers/react-wrapper
npm run test --prefix wrappers/vue3
npm run test --prefix wrappers/angular-wrapper
3. Fill the PR Template
The repository has a PR template at .github/PULL_REQUEST_TEMPLATE.md. Fill in each section:
- Context -- Explain why the change is needed, not just what changed. Link the ClickUp task or GitHub issue.
- How has this been tested? -- List the specific tests you added or ran (unit, E2E, manual). Include commands someone can copy-paste to reproduce.
- Types of changes -- Check the box that applies: bug fix, new feature, breaking change, or translation.
- Related issue(s) -- Link GitHub issues with
#xxx. Include ClickUp task IDs (e.g.DEV-627) so they auto-link. - Affected project(s) -- Check every package your change touches:
handsontable,@handsontable/react-wrapper,@handsontable/angular-wrapper,@handsontable/vue3. - Checklist -- Confirm code style, CLA signature, and whether documentation needs updating. The CLA is checked automatically by the required
cla/signedstatus check; one signature covers Handsontable and HyperFormula. SeeCONTRIBUTING.md.
4. Target Branch
All PRs target the develop branch. Cherry-picks to release/* or lts/* branches are handled separately by maintainers.
5. Create the PR First (before the changelog)
The PR is created before the changelog entry. The changelog file is named after the PR number, so you need the number the GitHub API returns from gh pr create before you can write the file correctly. Creating the changelog first and guessing the next PR number is unreliable — other PRs can be opened between your check and your push.
Workflow:
- Commit the code change on the feature branch.
- Push the branch (use
gh auth setup-gitonce if git is configured for SSH and the SSH key is unavailable in the session; then push over HTTPS). - Run
gh pr createand capture the returned PR URL / number. - Use that PR number when creating the changelog entry (next step).
Use the GitHub CLI to create the PR. Always create PRs as drafts -- the author marks it ready for review when appropriate.
Authentication fallback: If git push fails with Permission denied (publickey) because the session has no SSH key, switch the remote to HTTPS and let gh provide credentials, then push:
git remote set-url origin https://github.com/handsontable/handsontable.git
gh auth setup-git
git push -u origin <branch-name>
Always write the PR body to a temp file and use --body-file. Never use --body "$(cat <<'EOF'...EOF)" — backticks inside a heredoc passed through shell command substitution are stored as literal ``` characters in GitHub, breaking all inline code formatting in the PR description.
Use a unique, task-scoped temp filename — not a fixed /tmp/pr-body.md. The Write tool refuses to overwrite a file it has not read in the current session, so a stale /tmp/pr-body.md left over from an earlier session makes the write fail with "Error writing file." Name the file after the branch's task/issue ID so it is both unique per PR and easy to trace: /tmp/pr-body-DEV-1860.md, /tmp/pr-body-issue-11832.md. If that path somehow already exists, append a short unique suffix (e.g. /tmp/pr-body-DEV-1860-2.md).
The correct workflow:
- Write the body to
/tmp/pr-body-<task-id>.mdusing the Write tool (no shell escaping needed). - Pass it with
--body-file /tmp/pr-body-<task-id>.md.
# Step 1: write body to file first (use the Write tool, not shell echo/cat)
# e.g. /tmp/pr-body-DEV-1860.md
# Step 2: create the PR
gh pr create --draft --base develop \
--title "DEV-xxx: Short description" \
--body-file /tmp/pr-body-DEV-xxx.md
Start from the live template, every time. Run cat .github/PULL_REQUEST_TEMPLATE.md and mirror every ### heading and every checklist line it carries — including MANUAL QA NEEDED, left unticked when no manual QA is needed. That line is machine-read by the Checks scope router (checks.yml), so keep its wording, and its absence means the gate can never be armed on that PR without editing the description. The copy below is a convenience: .github/scripts/__tests__/pr-template-skill-sync.test.mjs pins its headings and checklist lines to the template, and when the two disagree the template wins.
The body file template (write this with the Write tool, backticks and all, no escaping):
### Context
The PR fixes/adds/changes <what>. <Why the change is needed; link the task and explain the problem.>
### Test evidence (required for source changes)
- Unit tests added/modified (`*.unit.js`): <paths, or "none — covered by <path>">
- E2E tests added/modified (Playwright `tests/e2e/*.spec.ts`): <paths>
- Type tests (`*.types.ts`) updated if public API changed: <paths, or "none">
- For a bug fix — the spec that fails without this fix: <name>
- Demo page / recorded trace (for UI changes): <link, or "none">
### Commands run
```bash
<the test commands you ran, one per block>
```
<their final output lines>
### Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature or improvement (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Additional language file or change to the existing one (translations)
### Related issue(s):
1. DEV-xxx
### Affected project(s):
- [x] `handsontable`
- [ ] `@handsontable/angular-wrapper`
- [ ] `@handsontable/react-wrapper`
- [ ] `@handsontable/vue3`
### Checklist:
- [x] I have reviewed the guidelines about [Contributing to Handsontable](https://github.com/handsontable/handsontable/blob/master/CONTRIBUTING.md) and I confirm that my code follows the code style of this project.
- [x] I have signed the [Contributor License Agreement](https://cla.handsontable.com/sign) — one signature covers both Handsontable and HyperFormula; the `cla/signed` check on this PR confirms it.
- [ ] My change requires a change to the documentation.
- [ ] MANUAL QA NEEDED — <!-- one line: WHAT to check and why automation can't judge it. Also add the red `Requires Manual QA` label (that exact name — it already exists; `QA needed` and `Verified by QA` are different labels). Ticking holds the Tests run for a manual-qa environment approval by a designated reviewer (never whoever triggered the run). The box is read once per run, so if you change it after the pipeline ran, press "Re-run all jobs". This line is machine-read — keep its wording. -->
ClickUp task: https://app.clickup.com/t/9015210959/DEV-xxx
Commit messages: Descriptive, max 80 characters. Include task ID (e.g.
DEV-627: Fix filter column index).Include the ClickUp task ID in the PR title when applicable.
Start the Context section with "The PR fixes/adds/changes/..." -- be direct, no filler.
If the PR introduces a breaking change, require the
Breaking changelabel and include a migration section with before/after examples. Update migration guides indocs/content/guides/upgrade-and-migration/.If you tick "MANUAL QA NEEDED" in the checklist, also apply the red
Requires Manual QAlabel so the request is visible in the PR list. Nothing applies it automatically — labels in this repo are applied by hand:gh pr edit <number> --add-label "Requires Manual QA"The label already exists in this repository — do not create it.
gh label list --search "Manual QA"also returnsQA neededandVerified by QA, which are different labels with their own meanings, so match the name exactly rather than the closest hit. Creating a near-miss name (Manual QA required) silently makes a second red label that nobody filters on.The label is a marker only. The gate is the ticked box, which the Checks scope router reads when the pipeline starts: it holds
Manual QA / sign-offuntil a designated reviewer approves the run. Because the box is read once per run, ticking it after a pipeline has already gone green does not arm anything — press "Re-run all jobs" on the Tests run (and the same applies in reverse after unticking).
5a. Updating an Existing PR's Body
When asked to update, fix, or re-fill a PR description, use the same temp-file approach with a unique, task-scoped filename: write the body to /tmp/pr-body-<task-id>.md (e.g. /tmp/pr-body-DEV-1860.md) with the Write tool, then gh pr edit <number> --body-file /tmp/pr-body-<task-id>.md. A fixed /tmp/pr-body.md fails when a stale copy from an earlier session exists, because the Write tool will not overwrite a file it has not read this session. Keep the full template structure — do not replace it with a shorter summary. Never use --body "$(cat <<'EOF'...EOF)" — backticks are not shell-escaped in the Write tool output and will be stored as literal ``` on GitHub.
6. Changelog Entry (after PR is created)
Every PR that changes source code needs a changelog entry in .changelogs/. bin/changelog names the file after the entry's issueOrPR field, so the filename is the PR number only for a private entry — the default, and what the rest of this section assumes. A public entry is named after its GitHub issue number instead, and because that number is known before the PR exists, it can be committed together with the code rather than in the round-trip below.
Which issuesOrigin to use is decided by .changelogs/README.md, not here — read it before writing the entry.
Two blocking checks constrain the entry, so get both right the first time. The filename must be <issueOrPR>.json, a plain number with no suffix, and it must match the entry's issueOrPR field — bin/changelog fails the changelog job over a mismatch, and the pre-push hook fails locally. And the PR may add at most two entry files, the second only for a separate GitHub issue it closes; a maintenance PR back-filling entries for other PRs writes [multiple changelogs] in the description to lift that. Running npm run changelog entry satisfies the filename rule by construction; writing the JSON by hand is what breaks it.
For a private entry, after writing the file:
- Commit it on the same branch (
DEV-xxx: Add changelog entry for PR #<number>). - Push so the PR picks up the new commit.
Use [skip changelog] in the PR body only for test-only, docs-only, or CI/tooling changes. When skipping, you do not create a PR-first round-trip — just open the PR and be done.
7. After PR Creation
When working from a ClickUp task, use the ClickUp MCP tools to update the task status to "code review".
8. Merge Strategy
All PRs are merged using "Squash and merge". The squashed commit message becomes the permanent history, so make sure the PR title is clear and descriptive.