Lab 2.2: Prepare a PR Using Firecracker Practices

Background

This lab is the full mechanical pipeline of a Firecracker pull request, end to end, with a trivial change so the workflow — not the code — is the lesson. You will fork, clone, branch off main, make a tiny doc/comment change, format with tools/devtool fmt, make a signed commit (git commit -s for DCO), write a CHANGELOG.md entry, run tools/devtool checkstyle and checkbuild --all, push to your fork, and open a PR against main. You will see a real diff, a real Signed-off-by trailer, the PR template, the bots, and how to respond to review by amending and force-pushing.

Do this once with a throwaway change so that when you do Lab 2.3 for real, the plumbing is invisible and all your attention is on the change.

Why This Lab Matters for Contributors

  • Every Firecracker PR you ever open follows this exact sequence. Internalize it and you stop thinking about mechanics and start thinking about the change.
  • The DCO bot and the tools/devtool gates are blocking. Getting them green locally avoids the two most common first-PR failures: an unsigned commit and a clippy warning.
  • Firecracker keeps your commit history (no squash-on-merge). Learning to amend and force-push your own branch cleanly — rather than piling on "fix review" commits — is what a reviewer expects.

Prerequisites

  • Lab 2.1 complete; you can navigate the repo and run tools/devtool.
  • 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 firecracker-microvm/firecracker to your account (the Fork button, 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/firecracker.git
cd firecracker

# Add the canonical repo as 'upstream' so you can keep main current:
git remote add upstream https://github.com/firecracker-microvm/firecracker.git
git remote -v
# origin    https://github.com/YOURNAME/firecracker.git (fetch/push)        <- your fork
# upstream  https://github.com/firecracker-microvm/firecracker.git (fetch/push)

gh does this in one step:

gh repo fork firecracker-microvm/firecracker --clone=true --remote=true

Step 2: Sync main and Branch

Always branch from an up-to-date main. Firecracker develops on main; you never branch off a release tag:

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 named after the change:
git checkout -b docs/fix-getting-started-curl

Note: One PR, one branch, one logical change. If you find a second thing to fix "while you're here," that is a second branch and a second PR. Scope creep is the fastest way to stall a first PR.

Step 3: Make a Trivial Change

For this dry run, pick something harmless and real — a stale command in docs/, a typo in a doc, or a clarifying word in a code comment. Find a candidate by role, not by guessing:

# Look for a fixable doc nit (illustrative — inspect, then pick ONE small thing).
rg -n 'localhost' docs/getting-started.md | head
rg -n 'tools/devtool' docs/*.md | head

Suppose docs/getting-started.md shows an curl example missing the explicit -X PUT that every other example uses. Make the edit in your editor. The resulting diff should be small and obviously correct:

diff --git a/docs/getting-started.md b/docs/getting-started.md
index abc1234..def5678 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -120,7 +120,7 @@ Configure the boot source:

-curl --unix-socket "${API_SOCKET}" -i \
+curl -X PUT --unix-socket "${API_SOCKET}" -i \
     --data '{"kernel_image_path": "./vmlinux-6.1.x", ...}' \
     "http://localhost/boot-source"

This is deliberately tiny. The discipline you are practicing is "one logical change, obviously correct" — not the change itself.

Tip: If you touch a Rust file instead (say, fix a typo in a doc-comment), the same flow applies, but now tools/devtool fmt and clippy actually matter. Try both at least once.

Step 4: Format with tools/devtool fmt

Run the formatter first. It fixes most of what checkstyle would otherwise flag — cargo fmt, clippy --fix, cargo sort, and the Python/markdown formatters:

tools/devtool fmt
git diff --stat        # see what fmt changed; review it before committing

For a pure-Markdown change fmt may only run mdformat; for a Rust change it will reformat code and auto-fix safe clippy lints. Make running it a reflex.

Step 5: Write the CHANGELOG Entry

Open CHANGELOG.md, find the unreleased heading, and add one line in the correct subsection. Read the real section names from the file rather than trusting this example:

rg -n '^##? ' CHANGELOG.md | head -n 20      # find the unreleased heading and subsections

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]

 ### Added

 ### Changed

 ### Fixed

+- Corrected the `boot-source` `curl` example in `docs/getting-started.md` to use an explicit
+  `-X PUT` ([#NNNNN](https://github.com/firecracker-microvm/firecracker/pull/NNNNN))

You will not know the PR number yet — use #NNNNN and update it after the PR opens (push, read the assigned number, amend). CI checks that an entry exists, not that the number is final.

Note: Truly internal, non-user-facing changes may not need a CHANGELOG line. A doc or behavior change does. When in doubt, add one — a missing entry is the #1 first-PR nit on Firecracker, exactly as it is on every Keep-a-Changelog project.

Step 6: Run the Quality Gates

These three commands are the local mirror of CI. Run them before you commit; green here means green there:

tools/devtool checkstyle        # fmt --check, clippy -D warnings, license headers, py/md lint
tools/devtool checkbuild --all  # build the workspace across the target/libc matrix
# If you changed code (not just docs), also run the relevant tests:
# tools/devtool test -- integration_tests/functional/test_<area>.py

checkstyle is the one that catches people. Its clippy step is warnings-as-errors (cargo clippy --all --all-targets --all-features -- -D warnings); a single lint fails the whole job. If it flags something fmt didn't auto-fix, fix it by hand — do not #[allow] it to silence the gate without a real justification a reviewer will accept.

Step 7: Commit with DCO Sign-off

The -s flag is mandatory — it appends the Signed-off-by trailer the DCO bot requires:

git add docs/getting-started.md CHANGELOG.md
git commit -s -m "docs: use explicit -X PUT in the getting-started boot-source example"

Inspect the commit — note the trailer and that it matches your identity:

git log -1 --format=full
commit a1b2c3d4...
Author:     Your Name <your.email@example.com>
Commit:     Your Name <your.email@example.com>

    docs: use explicit -X PUT in the getting-started boot-source 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, or you have several commits to sign, fix history without redoing the work:

git commit --amend -s --no-edit          # add sign-off to the latest commit
git rebase --signoff upstream/main       # add sign-off to EVERY commit on the branch

Note on commit hygiene: Firecracker wants one logical change per commit, each commit building on its own, with a title ≤ ~72 chars in the imperative mood. If your branch grew messy, curate it before review: git rebase -i upstream/main to split/reorder/reword, then git rebase --signoff to re-sign anything you rewrote.

Step 8: Push and Open the PR

git push origin docs/fix-getting-started-curl

GitHub prints a "create a pull request" URL, or use gh:

gh pr create --repo firecracker-microvm/firecracker --base main \
  --title "docs: use explicit -X PUT in the getting-started boot-source example" \
  --body "Fills the template below."

When the PR opens, GitHub pre-fills .github/PULL_REQUEST_TEMPLATE.md. Read the real template (find .github -iname '*pull_request*' -exec cat {} +) and fill every section honestly. It looks approximately like this (verify on your branch):

## Changes

Corrects the `boot-source` `curl` example in `docs/getting-started.md` to include an explicit
`-X PUT`, matching the other examples. Documentation-only change.

## Reason

The example as written relied on curl inferring the method, which is inconsistent with the
surrounding examples and confusing for new users.

## License Acceptance

By submitting this pull request, I confirm that my contribution is made under the terms of the
Apache 2.0 license.

## PR Checklist

- [x] All commits in this PR are signed (`git commit -s`).
- [x] The reason for this PR is clearly provided.
- [x] The description of changes is clear and encompassing.
- [ ] Any required documentation changes are included.   <!-- this IS the doc change -->
- [ ] New functionality includes integration tests.       <!-- N/A: docs only -->
- [x] A CHANGELOG entry has been added (or is not needed).

Step 9: Read the Bots and CI Checks

Once opened, the bots and CI run. Each maps to something you can reproduce locally:

CheckVerifiesIf it's red
DCOEvery commit has a valid Signed-off-by matching the author.git commit --amend -s / git rebase --signoff, then git push --force-with-lease.
fmt / clippycargo fmt --check and cargo clippy … -D warnings are clean.tools/devtool fmt then tools/devtool checkstyle; fix; push.
build matrixThe workspace builds across x86_64/aarch64, debug/release, musl/gnu.tools/devtool checkbuild --all locally; fix; push.
testscargo test (unit) + the pytest integration suite in tests/.tools/devtool test; reproduce the failure locally; push.
style / coverageLicense headers, Python/markdown lint, coverage thresholds.tools/devtool checkstyle.
Kani (if your change is labeled Kani)Formal proofs over flagged code.Run the Kani target (verify) and fix the harness.

Note: Some CI jobs for a first-time contributor's PR wait for a maintainer to approve running them (a guard against executing arbitrary code). Be patient; do not spam pushes to retrigger.

Step 10: Respond to Review

A maintainer (from MAINTAINERS.md) reviews; Firecracker requires ≥2 approvals before a maintainer merges. The review style here differs from squash-merge projects: because the commit history is kept, you address feedback by editing the relevant commit and force-pushing your branch, not by stacking "address review" commits.

# Address a comment that belongs in commit 1 of 1:
# ...edit the file...
git add -A
git commit --amend -s --no-edit                 # fold the fix into the existing commit
git push --force-with-lease origin docs/fix-getting-started-curl

# If your PR has several commits and the fix belongs in an earlier one:
git rebase -i upstream/main                      # mark that commit 'edit', amend, continue
git rebase --signoff upstream/main               # ensure all are signed
git push --force-with-lease origin docs/fix-getting-started-curl

Reply to every review comment; mark conversations resolved when addressed. Keep main underneath you current while the PR sits in review:

git fetch upstream
git rebase upstream/main
git push --force-with-lease origin docs/fix-getting-started-curl

Warning: --force-with-lease on your own unmerged topic branch is correct and expected here. Never force-push a branch someone else is collaborating on, and never rewrite a maintainer's commits. Responding to feedback well is its own skill — Responding to Maintainer Feedback.


Implementation Requirements

Deliverables (open the PR as a draft and close it afterward if you don't want to actually submit a throwaway — 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 the unreleased heading in the correct subsection.
  • A clean tools/devtool fmt, tools/devtool checkstyle, and tools/devtool checkbuild --all.
  • 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 CI/bot check verifies and the local command that fixes a red.

Troubleshooting

DCO check is red: "Commit … does not have a valid sign-off"

A commit lacks (or mismatches) Signed-off-by. Fix every commit on the branch and force-push:

git rebase --signoff upstream/main
git push --force-with-lease origin docs/fix-getting-started-curl

If the email differs from your sign-off, set git config user.email to match and re-sign.

tools/devtool checkstyle fails on clippy in code you didn't touch

Confirm it is pre-existing on a clean main:

git stash
tools/devtool checkstyle
git stash pop

If main is clean, the failure is yours — fix the lint. If main is already red, that is an upstream issue, not your PR's; note it and proceed.

git merge --ff-only upstream/main fails

Your local main 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.

tools/devtool errors about Docker

tools/devtool runs the build/test inside a Docker container. Docker must be running and reachable (docker ps). Same prerequisite as Lab 1.1.

CHANGELOG check is red

You did not add the entry, or you put it in the wrong place. It must be under the unreleased heading in a valid subsection. Re-check with rg -n '^##? ' CHANGELOG.md.


Expected Output

A correct signed commit:

$ git log -1 --format='%an <%ae>%n%n%B'
Your Name <your.email@example.com>

docs: use explicit -X PUT in the getting-started boot-source example

Signed-off-by: Your Name <your.email@example.com>

A green local gate:

$ tools/devtool checkstyle && tools/devtool checkbuild --all
...
checkstyle: OK
build: x86_64-unknown-linux-musl OK ... release OK

(Exact output lines are version-sensitive — verify on your branch.)


Stretch Goals

  1. Read the PR template source. find .github -iname '*pull_request*' -exec cat {} + and map each checkbox to a CI/bot check or a maintainer expectation.
  2. Find the CI definitions. ls .github/workflows/ 2>/dev/null or, since Firecracker uses Buildkite, find . -path '*buildkite*' and rg -n 'devtool' .buildkite 2>/dev/null. Confirm the CI calls the same tools/devtool gates you run locally.
  3. Amend vs. new commit drill. Practice both: git commit --amend -s (rewrites the last commit) and a fresh git commit -s (adds one). Then git rebase -i to squash the new one into the first.
  4. Split a two-part change. Make a branch that does two things, then git rebase -i to split it into two one-logical-change commits, each signed and each building.
  5. Inspect the DCO history of a real PR. gh pr view <num> --repo firecracker-microvm/firecracker on a recently merged PR; look at how its commits are structured and signed.

Validation / Self-check

You are done when you can answer these without notes:

  1. What does git commit -s add, why is it required, and what must it match?
  2. Where exactly does a CHANGELOG entry go, and what happens in CI if it is missing?
  3. Which three tools/devtool commands gate a PR locally, and which CI check does each mirror?
  4. Why does Firecracker's clippy step fail on a warning? What is the exact CI clippy invocation?
  5. Why do you branch off main and never a release tag?
  6. During review, how do you fold a fix into an existing commit and update the PR — and when is force-pushing your branch acceptable versus not?
  7. How many maintainer approvals does a merge require, and who performs the merge?

Next: Lab 2.3 — Fix It: A Good First Issue, where the change is real.