Lab 2.2: Prepare a PR Using OpenSearch Practices
Background
This lab is the full mechanical pipeline of an OpenSearch pull request, end to end, with a trivial
change so the workflow — not the code — is the lesson. You will fork, clone, branch, make a tiny
doc/test change, add a CHANGELOG.md entry, format with Spotless, run precommit, make a signed
commit (git commit -s for DCO), push, and open a PR against main. You will see a real diff and
a real Signed-off-by commit message, understand the PR template and each CI check, and learn how
the backport 2.x label works.
Do this once with a throwaway change so that when you do Lab 2.3 for real, the plumbing is invisible.
Why This Lab Matters for Contributors
- Every PR you ever open follows this exact sequence. Internalize it and you stop thinking about mechanics and start thinking about the change.
- DCO and CHANGELOG are blocking CI checks; getting them right locally avoids the two most common first-PR failures.
- Running
spotlessApply+precommitlocally is what turns a five-round review into one.
Prerequisites
- Lab 2.1 complete; you can navigate the repo.
- A GitHub account,
gitconfigured, and theghCLI (optional but convenient). - Your git identity set correctly — this becomes your DCO sign-off:
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
git config user.name; git config user.email # verify; these MUST match your Signed-off-by
Step-by-Step Tasks
Step 1: Fork and Clone
Fork opensearch-project/OpenSearch to your account (the Fork button on GitHub, or
gh repo fork). Then clone your fork and wire the canonical repo as upstream:
# Clone your fork (replace YOURNAME):
git clone https://github.com/YOURNAME/OpenSearch.git
cd OpenSearch
# Add the canonical repo as 'upstream' so you can keep main current:
git remote add upstream https://github.com/opensearch-project/OpenSearch.git
git remote -v
# origin https://github.com/YOURNAME/OpenSearch.git (fetch/push) <- your fork
# upstream https://github.com/opensearch-project/OpenSearch.git (fetch/push)
gh does this in one step:
gh repo fork opensearch-project/OpenSearch --clone=true --remote=true
Step 2: Sync main and Branch
Always branch from an up-to-date main:
git checkout main
git fetch upstream
git merge --ff-only upstream/main # fast-forward your local main to upstream
git push origin main # keep your fork's main current too
# Create a topic branch (name it after the change):
git checkout -b docs/clarify-bulk-example
Note: Branch off
main. Fixes land onmainfirst and are backported to2.xlater via thebackport 2.xlabel — you do not branch off2.xfor a new fix.
Step 3: Make a Trivial Change
For this dry run, pick something harmless and real — for example, fix a small inaccuracy or add a clarifying sentence to an in-repo doc, or tighten a test's assertion message. Keep it to one logical change. Suppose you correct a stale example in a developer doc:
# Find a candidate (illustrative grep — look for a fixable doc nit):
grep -rn "localhost:9200" DEVELOPER_GUIDE.md TESTING.md 2>/dev/null | head
Make the edit in your editor. The resulting diff should be small and obviously correct:
diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md
index abc1234..def5678 100644
--- a/DEVELOPER_GUIDE.md
+++ b/DEVELOPER_GUIDE.md
@@ -212,7 +212,7 @@ To run a single test class:
- ./gradlew test --tests "org.opensearch.ExampleTests"
+ ./gradlew :server:test --tests "org.opensearch.ExampleTests"
This is deliberately tiny. The discipline of "one logical change, obviously correct" is what you are practicing — not the change itself.
Step 4: Add a CHANGELOG Entry
Open CHANGELOG.md, find the ## [Unreleased ...] heading, and add one line in the appropriate
subsection (Added / Changed / Fixed / Deprecated / Removed). A doc fix usually goes under
Fixed or Changed:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1111aaa..2222bbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@
## [Unreleased 3.x]
### Added
### Changed
+- Correct the single-test example in the developer guide to use the `:server:test` task ([#NNNNN](https://github.com/opensearch-project/OpenSearch/pull/NNNNN))
### Fixed
You will not know the PR number yet — use a placeholder (#NNNNN) and update it after the PR opens,
or many contributors push, read the assigned number, then amend. CI checks that an entry exists,
not that the number is final.
Note: Truly trivial, non-user-facing changes can sometimes carry the "skip changelog" label instead of an entry — but default to adding one. A missing CHANGELOG is the #1 first-PR nit.
Step 5: Format with Spotless
Even for a doc/test change, run Spotless so formatting never blocks you:
./gradlew spotlessApply # auto-format any Java you touched
./gradlew spotlessJavaCheck # verify (this is what CI checks)
For a pure-Markdown change this is a no-op for the formatter, but make it a reflex — the moment you
touch a .java, Spotless matters.
Step 6: Run Precommit (and Scoped Tests if You Touched Code)
./gradlew precommit
# If you changed a test or source file, also run the scoped test:
# ./gradlew :server:test --tests "org.opensearch.ExampleTests"
A green precommit locally means the CI precommit check will be green too. This is the highest-
leverage habit in the whole workflow (from Lab 1.2).
Step 7: Commit with DCO Sign-off
The -s flag is mandatory — it appends the Signed-off-by line the DCO check requires:
git add DEVELOPER_GUIDE.md CHANGELOG.md
git commit -s -m "Use the :server:test task in the single-test developer-guide example"
Inspect the resulting commit — note the trailer:
git log -1 --format=full
commit a1b2c3d4...
Author: Your Name <your.email@example.com>
Commit: Your Name <your.email@example.com>
Use the :server:test task in the single-test developer-guide example
Signed-off-by: Your Name <your.email@example.com>
The Signed-off-by name/email must match your git identity. If you forgot -s, fix it without
re-doing the work:
git commit --amend -s --no-edit # add sign-off to the latest commit
# or, across multiple commits on the branch:
git rebase --signoff upstream/main
Step 8: Push and Open the PR
git push origin docs/clarify-bulk-example
GitHub prints a "create a pull request" URL, or use gh:
gh pr create --repo opensearch-project/OpenSearch --base main \
--title "Use the :server:test task in the single-test developer-guide example" \
--body "See template below."
When the PR opens, GitHub pre-fills .github/pull_request_template.md. Fill every section honestly:
### Description
Corrects the single-test example in DEVELOPER_GUIDE.md to use the `:server:test`
Gradle path, matching the project layout. Pure documentation change.
### Related Issues
Resolves #NNNNN <!-- or "N/A" if you filed no issue for a trivial doc fix -->
### Check List
- [x] New functionality includes testing. (N/A — docs only)
- [x] New functionality has been documented.
- [x] API changes companion pull request created, if applicable.
- [x] Commits are signed per the DCO using `--signoff`.
- [x] Changelog updated, or "skip changelog" justified.
Step 9: Read the CI Checks
Once opened, CI runs. Each check maps to something you can reproduce locally:
| CI check | Verifies | If it's red |
|---|---|---|
| DCO | Every commit has a valid Signed-off-by. | git commit --amend -s / git rebase --signoff, force-push the branch. |
| Changelog | An entry exists under ## [Unreleased] (or skip label). | Add the line; push. |
| precommit | Checkstyle, forbidden APIs, SPDX headers, etc. | ./gradlew precommit locally; fix; push. |
| assemble | The distribution still builds. | ./gradlew assemble. |
| gradle-check | Build + relevant tests + precommit across affected projects. | Reproduce the failing test locally with its -Dtests.seed. |
Note: Some CI workflows require a maintainer to comment to start the heavy
gradle-checkon a first-time contributor's PR (a security gate against running arbitrary code). Be patient; do not spam pushes to retrigger.
Step 10: Respond to Review and the Backport Label
A maintainer (listed in MAINTAINERS.md) reviews. For each comment:
- Make the change as a new signed commit on the same branch and push. Do not force-push away the review history unless a maintainer asks you to squash.
- Reply to each comment, marking resolved when addressed.
# Address a review comment:
# ...edit...
git add -A && git commit -s -m "Address review: reword the example caption"
git push origin docs/clarify-bulk-example
When approved, a maintainer squash-merges to main. If the fix should also go to the maintenance
line, the backport 2.x label is applied (by a maintainer, or by you if you have the rights).
A bot then opens a backport PR cherry-picking your squashed commit onto 2.x; you may need to
resolve conflicts there. Responding to feedback well is its own skill —
Responding to Maintainer Feedback.
Implementation Requirements
Deliverables (you may open the PR as a draft and close it afterward — the goal is the mechanics):
-
A fork with
upstreamconfigured and a localmainfast-forwarded toupstream/main. -
A topic branch off
mainwith one small, obviously-correct change. -
A
CHANGELOG.mdentry under## [Unreleased]. -
A clean
./gradlew spotlessApplyand./gradlew precommit. -
A commit whose
git log -1 --format=fullshows aSigned-off-bymatching your git identity. - A pushed branch and an opened (draft) PR with the template fully filled in.
- A written description of what each of the five CI checks verifies and how to fix a red one.
Troubleshooting
DCO check is red: "Commit sha … does not have a valid sign-off"
The commit lacks (or has a mismatched) Signed-off-by. Fix and force-push the branch:
git rebase --signoff upstream/main # add sign-off to every commit on the branch
git push --force-with-lease origin docs/clarify-bulk-example
(Force-pushing your own unmerged topic branch to fix DCO/rebase is fine; force-pushing away a maintainer's in-progress review is not.)
Changelog check is red
You did not add (or you mis-placed) the entry. It must be under the ## [Unreleased] heading in the
correct subsection. Re-check the heading name matches your target line (3.x for main).
git merge --ff-only upstream/main fails
Your local main has diverged (you committed on it by accident). Reset it to upstream:
git checkout main
git fetch upstream
git reset --hard upstream/main
Never commit on main; always branch.
precommit fails on a file you did not touch
Confirm it is pre-existing on a clean main (git stash; ./gradlew precommit; git stash pop). If
main is clean, the failure is yours — fix it.
Expected Output
A correct signed commit:
$ git log -1 --format='%an <%ae>%n%n%B'
Your Name <your.email@example.com>
Use the :server:test task in the single-test developer-guide example
Signed-off-by: Your Name <your.email@example.com>
A green local gate:
$ ./gradlew spotlessJavaCheck precommit
BUILD SUCCESSFUL in 5m 12s
Stretch Goals
-
Practice the backport mentally. Read
.github/workflows/for the backport workflow file andgrep -rn "backport" .github/. Identify which label triggers the bot. -
Inspect the PR template source.
cat .github/pull_request_template.mdand map each checkbox to a CI check or a maintainer expectation. -
Amend versus new commit. Practice both:
git commit --amend -s(rewrites the last commit) and a freshgit commit -s(adds a new one). Know when each is appropriate during review (new commits during review; amend only before the first push or when asked to squash). -
Keep a long-lived branch current. While your PR sits in review,
mainmoves. Practice:git fetch upstream && git rebase upstream/main && git push --force-with-lease.
Coding Exercises
The PR pipeline is code and tooling — git, Gradle, CHANGELOG format, and small scripts that mirror
the CI checks. These exercises have you write the automation a fluent contributor runs before every
push. Locate every file with find/rg; never cite a line number you did not just see.
-
(warm-up) A real doc/test micro-PR (carried through the whole pipeline). Make one genuinely small, correct change — tighten an assertion message in an existing test (
rg -n "assertEquals\(\"" server/src/test/java/org/opensearch/common | headfor a candidate), or fix a stale command inDEVELOPER_GUIDE.md/TESTING.md. Verify: the diff is one logical change,git diffshows nothing extraneous, and (if you touched a test) the scoped--testsrun is green. -
(warm-up) Write a correct CHANGELOG entry by hand. Open
CHANGELOG.md, find the## [Unreleased 3.x]heading and the right subsection (Added/Changed/Fixed/...), and add one line in the project's exact format, including the([#NNNNN](...))PR-link shape. Verify: your entry sits under the correct subsection and matches the punctuation/style of the lines around it (compare withrg -n "^- " CHANGELOG.md | head). Wrong subsection or wrong format is the #1 first-PR nit. -
(core) A pre-commit gate script. Write
pre-push-check.shthat runs, in order:./gradlew spotlessApply, then./gradlew spotlessJavaCheck, then./gradlew precommit, then — if any.javachanged (git diff --name-onlyfiltered to*.java) — the scoped:server:test. It mustexit 1the moment any step fails so you never push a red branch. Verify: it passes on a clean change and fails fast if you introduce a formatting violation (Exercise: add a stray double space, watchspotlessJavaCheckgo red). -
(core) A DCO-and-changelog verifier. Write
verify-pr-ready.shthat checks the branch (not just the working tree): every commit sinceupstream/mainhas aSigned-off-bymatchinggit config user.email(loopgit log upstream/main..HEAD --format='%H %ae'and grep each commit body for the trailer), AND the diff againstupstream/maintouchesCHANGELOG.md. It prints a checklist andexit 1s on any failure. Verify: it passes for a properly signed branch with a changelog entry, and fails if yougit commitwithout-s(then confirmgit rebase --signoff upstream/mainmakes it pass). -
(core) A
spotlessApplyround-trip proof. Write a script that deliberately mis-formats a throwaway.javafile (e.g. collapse some braces withsedin a scratch copy), runs./gradlew spotlessApply, and asserts the file changed back to canonical form (git diff --quietis now true after re-staging, or the file content matches the formatter's output). Verify: the script proves Spotless rewrote the file — encoding Step 5 as a check you can trust. -
(advanced) Advanced challenge — a one-command "open-PR-ready" tool. Compose the pieces into
prep-pr.sh <branch-name>that: ensures localmainis fast-forwarded toupstream/main; creates the topic branch; runs the Exercise-3 gate; runs the Exercise-4 verifier; and, only if everything is green, prints the exactgit push origin <branch>andgh pr create ...commands (with the--base mainand template flags) for you to run — it must refuse to print them if any gate failed. Then dry-run it against a real throwaway change and open a draft PR with.github/pull_request_template.mdfilled in. Verify: the tool blocks on a missing CHANGELOG or missing sign-off, and the draft PR you open passes the DCO, Changelog, and precommit CI checks. This is the personal pre-flight every productive OpenSearch contributor builds for themself.
Issues to Practice On
This lab is the mechanics; the issues to practice the mechanics on are the gentlest real
contributions in opensearch-project/OpenSearch — docs, changelog, and "good first issue" work
where the workflow is the whole challenge.
# The friendliest starting points:
gh issue list --repo opensearch-project/OpenSearch --label "good first issue" --state open
# Docs / changelog / process-flavored issues (labels move; confirm on the tracker):
gh issue list --repo opensearch-project/OpenSearch --label "documentation" --state open
gh label list --repo opensearch-project/OpenSearch | grep -iE "doc|good first|changelog|backport"
Representative issue patterns for this subsystem:
- A documentation or developer-guide fix. The ideal first PR: small, obviously correct, no behavior
change. Reproduce the inaccuracy (does the documented command actually work?), fix it, add a
CHANGELOG entry under
Changed/Fixed, sign off, and open the PR. The process is the deliverable. - A "skip changelog" judgment call. Some trivial, non-user-facing changes carry the
skip-changeloglabel instead of an entry. Practice deciding: default to adding an entry; only skip when the change is genuinely invisible to users — and justify it in the PR.
Planted-bug drill. Sabotage your own PR-readiness and let your tooling catch it before CI does:
- Make a valid change, then forget the sign-off:
git commit -m "..."(no-s). Run your Exercise-4 verifier and watch it flag the missingSigned-off-by. Fix withgit commit --amend -s --no-edit; re-run; green. (This is the DCO CI check, reproduced locally.) - Then forget the CHANGELOG: stage your code change but not a
CHANGELOG.mdline. Run the verifier and watch the changelog check fail. Add the entry; re-run; green. You have now reproduced the two most common first-PR CI failures before pushing — exactly what Lab 2.4 will flag in someone else's PR.
Etiquette: claim the issue, reproduce first, one logical change per PR, and always test +
CHANGELOG + DCO Signed-off-by. Responding well to the review that follows is its own skill —
Responding to Maintainer Feedback and
community interaction.
Validation / Self-check
You are done when you can answer these without notes:
- What does
git commit -sadd, and why is it required? What must it match? - Where exactly does a CHANGELOG entry go, and what happens in CI if it is missing?
- Which two Gradle tasks should you run locally before pushing, and why does that speed up review?
- Why do you branch off
mainand not2.xfor a new fix? How does the fix reach2.x? - When is force-pushing your branch acceptable, and when is it not?
- Name the five CI checks and, for each, the local command that reproduces it.
Next: Lab 2.3 — Fix It: A Good First Issue, where the change is real.