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 + precommit locally is what turns a five-round review into one.

Prerequisites

  • Lab 2.1 complete; you can navigate the repo.
  • A GitHub account, git configured, and the gh CLI (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 on main first and are backported to 2.x later via the backport 2.x label — you do not branch off 2.x for 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 checkVerifiesIf it's red
DCOEvery commit has a valid Signed-off-by.git commit --amend -s / git rebase --signoff, force-push the branch.
ChangelogAn entry exists under ## [Unreleased] (or skip label).Add the line; push.
precommitCheckstyle, forbidden APIs, SPDX headers, etc../gradlew precommit locally; fix; push.
assembleThe distribution still builds../gradlew assemble.
gradle-checkBuild + 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-check on 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 upstream configured and a local main fast-forwarded to upstream/main.
  • A topic branch off main with one small, obviously-correct change.
  • A CHANGELOG.md entry under ## [Unreleased].
  • A clean ./gradlew spotlessApply and ./gradlew precommit.
  • A commit whose git log -1 --format=full shows a Signed-off-by matching 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

  1. Practice the backport mentally. Read .github/workflows/ for the backport workflow file and grep -rn "backport" .github/. Identify which label triggers the bot.

  2. Inspect the PR template source. cat .github/pull_request_template.md and map each checkbox to a CI check or a maintainer expectation.

  3. Amend versus new commit. Practice both: git commit --amend -s (rewrites the last commit) and a fresh git 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).

  4. Keep a long-lived branch current. While your PR sits in review, main moves. 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.

  1. (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 | head for a candidate), or fix a stale command in DEVELOPER_GUIDE.md/TESTING.md. Verify: the diff is one logical change, git diff shows nothing extraneous, and (if you touched a test) the scoped --tests run is green.

  2. (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 with rg -n "^- " CHANGELOG.md | head). Wrong subsection or wrong format is the #1 first-PR nit.

  3. (core) A pre-commit gate script. Write pre-push-check.sh that runs, in order: ./gradlew spotlessApply, then ./gradlew spotlessJavaCheck, then ./gradlew precommit, then — if any .java changed (git diff --name-only filtered to *.java) — the scoped :server:test. It must exit 1 the 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, watch spotlessJavaCheck go red).

  4. (core) A DCO-and-changelog verifier. Write verify-pr-ready.sh that checks the branch (not just the working tree): every commit since upstream/main has a Signed-off-by matching git config user.email (loop git log upstream/main..HEAD --format='%H %ae' and grep each commit body for the trailer), AND the diff against upstream/main touches CHANGELOG.md. It prints a checklist and exit 1s on any failure. Verify: it passes for a properly signed branch with a changelog entry, and fails if you git commit without -s (then confirm git rebase --signoff upstream/main makes it pass).

  5. (core) A spotlessApply round-trip proof. Write a script that deliberately mis-formats a throwaway .java file (e.g. collapse some braces with sed in a scratch copy), runs ./gradlew spotlessApply, and asserts the file changed back to canonical form (git diff --quiet is 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.

  6. (advanced) Advanced challenge — a one-command "open-PR-ready" tool. Compose the pieces into prep-pr.sh <branch-name> that: ensures local main is fast-forwarded to upstream/main; creates the topic branch; runs the Exercise-3 gate; runs the Exercise-4 verifier; and, only if everything is green, prints the exact git push origin <branch> and gh pr create ... commands (with the --base main and 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.md filled 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-changelog label 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:

  1. Make a valid change, then forget the sign-off: git commit -m "..." (no -s). Run your Exercise-4 verifier and watch it flag the missing Signed-off-by. Fix with git commit --amend -s --no-edit; re-run; green. (This is the DCO CI check, reproduced locally.)
  2. Then forget the CHANGELOG: stage your code change but not a CHANGELOG.md line. 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:

  1. What does git commit -s add, and why is it required? What must it match?
  2. Where exactly does a CHANGELOG entry go, and what happens in CI if it is missing?
  3. Which two Gradle tasks should you run locally before pushing, and why does that speed up review?
  4. Why do you branch off main and not 2.x for a new fix? How does the fix reach 2.x?
  5. When is force-pushing your branch acceptable, and when is it not?
  6. 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.