CI Basics for Java: GitHub Actions from Scratch
Executive Summary
CI basics for Java start with one workflow file at .github/workflows/ci.yml in your repository. It runs on triggers you declare, and on push and on pull request are the standard CI pair. The file defines jobs that run on fresh GitHub-hosted runners. A Java job has three steps. First, actions/checkout copies the repository onto the runner. Next, actions/setup-java installs a pinned JDK, Temurin 21, exactly what the setup article installed by hand. Finally, a run step executes mvn clean package or mvn verify. That is the same command from the Maven article, now with consequences.
The green or red result posts on the commit and pull request. Enabling branch protection then makes the check required, so a red build blocks the merge mechanically. Speed comes from caching the Maven local repository, keyed by the pom hash. As a result, the second run downloads nothing, and the caching docs cover the store.
The growth path is additive. You add coverage reporting, artifact upload, and container builds as extra steps. Likewise, a slower integration job runs on pull requests only, all on the same skeleton. Finally, a few rules keep CI credible. Keep pipelines under ten minutes, and pin versions everywhere. Store secrets in GitHub secrets, never in YAML. Above all, make the gate actually required, because a bypassable check trains the team to bypass it.
Your First Workflow: Annotated
Here is the complete CI file for this course’s Maven projects. Comments on every line help readers new to YAML workflows:
# .github/workflows/ci.yml
name: CI # the name on every check in the UI
on: # WHEN this runs:
push:
branches: [main] # every push to main
pull_request: # every update to any pull request
jobs:
build:
runs-on: ubuntu-latest # a fresh Linux VM, thrown away after
steps:
- uses: actions/checkout@v4 # step 1: copy the repo onto the runner
- uses: actions/setup-java@v4 # step 2: install the JDK
with:
distribution: temurin # same JDK family as the setup article
java-version: '21' # pinned: no version roulette
- name: Build and test # step 3: the pipeline itself
run: mvn --batch-mode clean verify
# clean build, all tests, fail = red
- name: Upload test reports # step 4: evidence, always, on failure
uses: actions/upload-artifact@v4
if: always() # even when the build fails
with:
name: test-reports
path: target/surefire-reports/
Commit that file, push, and watch the repository’s Actions tab. A runner boots, three steps scroll past, and the commit gets its first green check. Next, open a pull request, and the same workflow posts its result directly on the PR. At that moment, CI becomes a conversation partner. Reviewers see the evidence before they look at a single line.
CI Basics for Java: Triggers, Jobs, and Steps
The vocabulary is small, yet the whole Actions ecosystem grows from it:
| Keyword | What it is | Notes for a Java project |
|---|---|---|
| on | Triggers: push, pull_request, schedule, manual | push to main plus pull_request is the CI baseline |
| jobs | Units of work, running in parallel by default | build now, integration and coverage jobs later |
| runs-on | The machine a job gets | ubuntu-latest unless you need macOS or a matrix |
| steps | Sequential commands or reusable actions inside a job | checkout, setup-java, then your mvn command |
| uses | A reusable action, someone else’s step | checkout, setup-java, upload-artifact: the standard kit |
Two structural facts matter. First, jobs run in parallel. So splitting build and a slower integration tier gives you a fast first result and a slower thorough one. They appear as two checks instead of one long wait. Second, steps run in order inside a job and fail fast, so a broken compile never runs tests. This matches the Maven article’s phase prefix behavior. Thus, mvn verify simply means everything through the integration checks in one command.
Caching: Making the Second Run Fast
The first CI run downloads the whole dependency graph, using Maven’s nearest-wins resolution from the Maven article. Every run after that pays the same minutes-long tax unless you cache. setup-java does it in one line:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven # caches ~/.m2/repository, keyed by the pom hash
The cache key comes from your pom.xml content. So adding a dependency changes the key, and the next run downloads exactly once again, then caches the new graph. Similarly, for Gradle projects the same action caches the Gradle caches with cache: gradle, and the Gradle article’s incremental machinery benefits identically. The reason this matters is behavioral. CI that takes two minutes gets watched, but CI that takes twenty gets ignored as it runs. In short, the good tests article’s speed discipline extends from the suite to the whole pipeline.
The Gate: Making Green Required
The workflow runs, but until it is required, it is advice. Branch protection turns it into law. In the repository settings, protect main, and require the CI status check to pass before merging. That single settings change converts the pipeline from a suggestion into a gate. In turn, the JUnit article’s failing-build-stops-everything promise becomes the merge policy. No green means no merge, with no exceptions except the emergency one everyone knows the cost of.
Growth from there is additive, and the skeleton never changes:
- Matrix builds: the same job across JDK versions or operating systems, strategy: matrix, if you support more than Java 21.
- Coverage: run the suite with a coverage tool and post the result on the PR. That comment keeps the number honest per change, rather than feeding a vanity dashboard.
- A slower tier: integration and end-to-end tests run as a separate job, on pull_request only. This keeps the first check fast, per the pyramid’s ratios.
- Artifacts: upload the built jar on main pushes. Later, releases publish from tagged builds, which is the deployment pipeline the capstone will use.
How Real Systems Do This
Every serious Java repository runs this exact shape. Teams require a fast build-and-test job on all pull requests, and a slower tier sits behind it. Meanwhile, the main branch stays permanently green, because a red main stops deploys for everyone. At scale, the standard extensions are self-hosted runners for cost and network access. Teams also keep secrets in the platform’s secret store and surface them as environment variables. Finally, pipeline badges in the README offer tiny public honesty about the project’s health.
The broken main that converted me on required checks is a story about goodwill failing exactly where process succeeds. A team I joined ran CI but did not require it, out of a kind culture. After all, nobody wants to block a colleague’s Friday deploy. Then one Friday, a merge landed with a passing local build, an unrun suite, and a dependency that only existed on its author’s laptop. Main stayed red until Monday morning, and every planned release slipped.
The retrospective’s finding was honest and damning. Every bypass had a good reason, and the accumulated good reasons cost two days. The fix was one settings checkbox: require the CI check on main. We added one human rule to make the checkbox humane. The on-call can merge with an explicit admin override that pages them if red persists an hour. Red mains went from quarterly events to none in the two years after. CI works when it is mandatory, and mandatory is a settings page, not a character trait.
Decision Framework
- Does the project have tests and a build command? It needs CI: the pipeline is the build command, on a runner, on every push.
- What triggers? push to main and all pull requests: the baseline pair, with nightly schedules for the heavyweight tiers.
- Which JDK? Pinned, in the workflow, matching the pom’s release setting. This is the setup article’s consistency rule, now enforced by a fresh machine.
- Is the run slower than ten minutes? Cache dependencies first, then split jobs, then move slow tiers behind labels or schedules.
- Is the check required on main? If not, it is advisory, and advisory red is a culture problem dressed as a tooling one.
- Do you need operating systems beyond Linux or multiple JDKs? A matrix, and only when you genuinely support them.
- Secrets needed? GitHub secrets surfaced as environment variables, never inline, and never echoed in logs.
When NOT to Use This
- Do not put secrets in workflow files or log output. The YAML is public in the repo, and Actions masks known secret patterns only when they came from the secret store.
- Do not run heavyweight end-to-end suites on every push. The pyramid’s ratios apply to pipelines, and teams bypass a 40-minute CI by the third flaky Tuesday.
- Do not self-host runners without thinking about security. A runner executing arbitrary pull request code is a server inside your network. Until you need otherwise, GitHub-hosted runners are the safe default.
- Do not copy workflows blindly. An action pinned by mutable tag can change under you, so pin the versions you have actually read.
- Do not let the workflow replace the suite’s own discipline. After all, a fast green pipeline over weak tests is a fast road to confident regressions.
Common Mistakes
- Unpinned JDK in the workflow. The build compiles against whatever the runner image shipped. It is the setup article’s version roulette, now in the cloud.
- No dependency caching: every run re-downloads the graph, CI drifts toward twenty minutes, and watching it becomes optional in practice.
- Triggers too broad. Running the full pipeline on every branch push burns minutes and attention, while the baseline pair covers what CI needs.
- Not requiring the check: an advisory gate trains bypasses, and the broken-main story above is where that road ends.
- YAML indentation errors: two spaces, consistent, because a mis-indented on block silently triggers nothing and the silence looks like green.
- Uploading test reports only on success. The reports matter most when the build fails, hence if: always() on the artifact step.
- Secrets echoed into logs. A printed token outlives the log rotation, so the fix is the secret store plus careful step output.
Key Takeaways
- CI is your build command on an indifferent machine. It runs mvn clean verify on a fresh runner, on every push and pull request, and posts the result on the commit.
- In CI basics for Java, a workflow has three steps. Check out the repository, run setup-java with a pinned JDK 21, and run the Maven or Gradle build. Also add an artifact step for failure evidence.
- Triggers are the baseline pair: push to main and all pull requests. Slow tiers sit behind labels, schedules, or separate jobs.
- Caching the dependency store makes the second run minutes faster, and fast pipelines get watched while slow ones get ignored.
- Branch protection makes the check required, converting the pipeline from advice into a gate, and advisory gates train bypasses.
- Secrets live in the platform’s secret store and surface as environment variables, never in YAML, never in logs.
- The growth path is additive on the same skeleton. Add coverage comments, matrix builds, a slower integration tier, and artifacts for deployment.
FAQ
What is CI in Java development?
Continuous integration means every push and pull request triggers the same build and test pipeline on a clean machine. The result then gates merges. For Java that pipeline is the Maven or Gradle build with the full test suite, run automatically by a service such as GitHub Actions.
How do I set up GitHub Actions for a Maven project?
Create .github/workflows/ci.yml with on push and pull_request triggers. Then add a job with three steps: actions/checkout, actions/setup-java with distribution temurin and java-version 21, and run: mvn –batch-mode clean verify. Push the file and the check runs on every subsequent push.
What does actions/setup-java do?
It installs a JDK on the runner. You pick the distribution, Temurin, and pin the version, 21. As a result, the pipeline compiles against exactly the JDK your pom targets. With cache: maven it also caches the local dependency repository, keyed by the pom.
How do I cache Maven dependencies in GitHub Actions?
Add cache: maven to the setup-java step. The runner then stores ~/.m2/repository keyed by the pom’s hash. So unchanged dependency graphs restore in seconds, and only new dependencies download. Gradle projects use cache: gradle identically.
What triggers should a Java CI pipeline use?
Use push to main plus all pull requests as the baseline. The first keeps main permanently green, and the second gives every proposed change a verdict before merge. Slower tiers move to pull_request-only jobs, nightly schedules, or manual dispatch.
Conclusion
With CI basics for Java in place, Part 7 is complete, and your Java now has its full production toolchain. Maven or Gradle turns source into artifacts, and JUnit and Mockito verify behavior. The suite engineering keeps the verification fast and honest, and logs narrate production. Debugging method handles the failures. Finally, CI binds all of it into a gate no merge can pass unverified.
Next, the course turns outward to the data your services will keep. It covers JDBC, connections, statements, and result sets, then pooling and transactions. A full CRUD application follows, building the persistence layer every production Java service stands on.
One file in .github/workflows changed your repository from a folder of code into a pipeline. Keep main green, and the rest is iteration.
Last updated on 13 September 2026.
