| name | iru-setup-java-springboot-github-workflows |
| description | Create the GitHub Actions workflows for a Spring Boot service repository — a build workflow (`build.yml`) that runs on pushes to the integration and main branches and on pull requests, executing unit tests via Surefire and Testcontainers-backed integration tests via Failsafe, Checkstyle/PMD/SpotBugs static analysis, aggregated JaCoCo coverage, a SonarCloud/SonarQube scan via `mvn sonar:sonar`, the Antora documentation build, and the generated API documentation — OpenAPI, protobuf, the AsyncAPI HTML produced by `@asyncapi/html-template`, and the GraphQL reference produced by `spectaql` and `graphql-voyager` — publishing all of it to GitHub Pages; a `deploy.yml` that builds and pushes the container image, authenticates to AWS or Google Cloud via keyless OIDC, and applies the OpenTofu configuration for a chosen environment behind a GitHub Environment approval gate; and an `undeploy.yml` that scales the service to zero or destroys an environment's infrastructure, guarded by a typed-confirmation input and a protected environment. Derives the pipeline from `springboot-stack.yml` and the actual reactor/`infra/` layout on disk, lists every required repository secret and environment, and never writes a credential into a workflow file. Invoke as `/iru-setup-java-springboot-github-workflows`, or with `args` (`stack-file:`, `integration-branch:` lines) when called from `iru-setup-java-springboot`. Use whenever a Spring Boot service needs its CI/CD pipeline bootstrapped instead of hand-writing the YAML for testing, analysis, documentation publishing, deployment, and teardown. |
| model | sonnet |
Setup Java Spring Boot GitHub Workflows
Generate three workflows for a Spring Boot service. This is deliberately a different pipeline from the
library-oriented iru-setup-java-github-workflows: a service is not published to Maven Central and is not
GPG-signed — it is built into a container image and deployed. The shared stages (test, analyse, Sonar, Antora,
GitHub Pages) follow the same house pattern as that skill, so the two are recognisably related; the publish/sign/
sync stages are replaced by image build, OpenTofu apply, and teardown.
| Workflow | Trigger | Does |
|---|
build.yml | push to integration/main branch, and every pull request | build, unit tests, integration tests, static analysis, coverage, Sonar, docs; publishes Pages from the main branch only |
deploy.yml | workflow_dispatch (environment input) and optionally on release published | build + push image, OIDC auth, tofu apply, smoke check |
undeploy.yml | workflow_dispatch only, with typed confirmation | scale to zero, or tofu destroy an environment |
No credential is ever written into a workflow file. Cloud access is keyless via OIDC; everything else is a
repository or environment secret referenced by name.
Step 0 — Survey and resolve inputs
Parse args for key: value lines:
stack-file: springboot-stack.yml
integration-branch: develop
main-branch: main
Read springboot-stack.yml for the cloud, the Java version, and which technologies are in play. Then survey what's
actually on disk — the workflows must match reality, not the manifest's intent:
- The reactor's modules, and the
boot module's directory (that's what produces the image).
- Whether
coverage/ exists and where report-aggregate writes (coverage/target/site/jacoco-aggregate/jacoco.xml)
— this is the path Sonar reads.
- Whether
sonar-maven-plugin and the sonar.* properties exist in the root pom. If not, the Sonar step will
fail; offer to add them (asking for sonar.organization/sonar.projectKey) or to omit the step.
- Whether
docs/antora.yml and docs/antora-playbook.yml exist, and which extensions docs/package.json lists
(the install step must match — including asciidoctor-kroki if the docs use Kroki).
- Where the API documentation generators write (
*/target/generated-docs/...), from the module poms.
- Whether
apis/graphql-server/ holds an SDL and an apis/graphql-server/docs/package.json, and whether that
toolchain includes graphql-voyager as well as spectaql — the docs job's GraphQL step must match what's
actually pinned there, and its package-lock.json must be committed for npm ci.
- Whether
apis/messaging/ holds an AsyncAPI specification and an apis/messaging/docs/package.json
(set up by iru-setup-java-springboot-apis). If so, the docs job must also produce the AsyncAPI HTML; note
whether apis/messaging/docs/package-lock.json is committed, since that decides npm ci versus npm install,
and which module's pom carries the frontend-maven-plugin executions.
- Whether
infra/envs/<env>/ directories exist and which environments they define. If infra/ doesn't exist,
still generate deploy.yml/undeploy.yml but mark the OpenTofu steps as requiring
/iru-setup-java-springboot-platform first, and say so in the report.
- The branch model: run
git branch -a, don't assume gitflow. Confirm the integration and main branch names.
- Whether GitHub Environments already exist (
gh api repos/{owner}/{repo}/environments) — deploy.yml and
depend on them for approval gates.
If any workflow file already exists, use AskUserQuestion to ask whether to stop or to update it, preserving any
step this skill doesn't own (a Slack notification, an extra matrix leg) and only correcting the stages it does.
Step 1 — build.yml
Structure it as two jobs so a pull request gets fast feedback and Pages publishing only happens on the main branch.
name: Build
on:
push:
branches: [ <integration-branch>, <main-branch> ]
pull_request:
branches: [ <integration-branch>, <main-branch> ]
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: Build, test and analyse
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up JDK <java-version>
uses: actions/setup-java@v5
with:
distribution:
Adjust to the survey: npm ci only works if docs/package-lock.json is committed — if it isn't, fall back to the
explicit npm i list iru-setup-antora uses, including asciidoctor-kroki if the playbook requires it, or a
Kroki-diagram page will fail the build. The mvn site invocation at the reactor root aggregates every module's
reports; verify the aggregate actually lands in ./target/site for this reactor (a multi-module site sometimes
needs site:stage) and use whichever path is real.
If the repository has no apis/messaging/, drop the AsyncAPI step, the -Dasyncapi.docs.skip=true flag, and the
second cache-dependency-path entry entirely rather than leaving dead YAML behind. If it does:
- The step is guarded by
hashFiles(...) != '' as well, so the workflow survives someone deleting the messaging
contracts without a workflow edit.
- The same
apis/messaging/docs/package-lock.json caveat applies: without a committed lock file npm ci fails
outright — commit it, or fall back to npm install, and say which in the report.
- The loop over
../*-async-api-v*.yaml picks up v2 automatically when it's added, so a new contract version
needs no workflow change. Keep the filename convention and the loop in step with each other.
- The published layout ends up as
doc/api/messaging/v1/index.html and
doc/api/graphql-server/v1/index.html (with the Voyager graph at
doc/api/graphql-server/v1/voyager/index.html), next to doc/api/rest-server/v1/ and
doc/api/grpc-server/v1/. Those are the URLs the Antora API pages link to, so changing them means updating
/iru-update-java-springboot-documentation's output too.
- Voyager's page loads
voyager.standalone.js and voyager.css by relative path, so those two files must be
copied alongside index.html. The merge step's directory copy preserves that automatically; a merge step
rewritten to copy only index.html files would silently produce a blank graph.
Step 2 — deploy.yml
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: Target environment
required: true
type: choice
options: [ dev, prod ]
image_tag:
description: Image tag to deploy (defaults to the commit SHA)
required: false
type: string
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
permissions:
contents: read
id-token: write
packages: write
jobs:
deploy:
name: Deploy to
Points to resolve from the survey rather than copy blindly:
- Registry authentication. For ECR, prefer
aws-actions/amazon-ecr-login (which yields a short-lived token
from the OIDC session) over REGISTRY_USERNAME/REGISTRY_PASSWORD secrets; for Artifact Registry, prefer
gcloud auth configure-docker. Use the OIDC-derived credential where the registry supports it and drop the
username/password secrets entirely — fewer stored secrets is strictly better. Only keep them for a third-party
registry.
apply runs the plan file that was just produced, so what's applied is exactly what was planned. Don't
replace this with a bare tofu apply -auto-approve.
- The approval gate lives in the GitHub Environment, not in the YAML. Tell the user to configure required
reviewers on the
prod environment — without that, workflow_dispatch on prod is one click from anyone with
write access.
- If deploying on release publication is wanted, add
release: types: [published] to the triggers with the
environment pinned to prod, and note that this removes the manual gate unless the Environment requires
reviewers.
- Buildpacks need a container daemon;
ubuntu-latest has one. If the org requires a daemonless build, note Jib as
the alternative and that it needs different plugin configuration in boot/pom.xml.
Step 3 — undeploy.yml
This workflow destroys things, so its guards are the feature, not the ceremony.
name: Undeploy
on:
workflow_dispatch:
inputs:
environment:
description: Environment to undeploy
required: true
type: choice
options: [ dev, prod ]
mode:
description: stop = scale the service to zero (infrastructure kept); destroy = delete all infrastructure
required: true
type: choice
options: [ stop, destroy ]
default: stop
confirmation:
description: Type the environment name exactly to confirm
required: true
type: string
concurrency:
Additional guards to put in, and to explain in the report:
stop is the default, and it's reversible: re-running deploy.yml brings the service back with its data
intact. destroy is not reversible.
- Data-bearing resources should have deletion protection enabled in the OpenTofu configuration, so
destroy
fails on them rather than silently deleting a production database. That means destroy on prod will error
until someone deliberately removes the protection — which is correct behaviour, not a bug. Say this explicitly.
- Recommend restricting
prod in the Environment settings to required reviewers and a deployment branch rule,
so this workflow can't be dispatched from an arbitrary branch.
- Consider omitting
prod from the destroy path entirely for a service holding real data, and say so as a
recommendation.
Step 4 — Required secrets, variables, and environments
Report these as a table; this skill cannot create them.
| Name | Kind | Purpose |
|---|
SONAR_TOKEN | repository secret | SonarCloud/SonarQube scan |
AWS_DEPLOY_ROLE_ARN | environment secret | OIDC role assumed per environment (AWS) |
GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_DEPLOY_SERVICE_ACCOUNT | environment secrets | OIDC identity (Google Cloud) |
AWS_REGION / GCP_PROJECT_ID, GCP_REGION | environment variables | not secret, but environment-specific |
IMAGE_REPOSITORY | environment variable | the registry path the image is pushed to |
<PROVIDER>_API_KEY | environment secret | LLM provider key, if stack.ai.enabled — never in the repository |
third-party provider tokens (MONGODB_ATLAS_*, CONFLUENT_CLOUD_*, …) | environment secrets | needed by the OpenTofu providers for managed services outside the cloud account |
GITHUB_TOKEN needs no setup. Also required, and easy to miss:
- A GitHub Environment per deployment environment, with required reviewers on
prod.
- GitHub Pages configured to serve from the
gh-pages branch (Settings → Pages), or the docs job publishes
into a branch nobody reads.
- The OIDC trust policy on the cloud side must name this repository and the environment. If it's scoped only
to the repository, any branch can deploy to production — flag this as a real finding if the platform skill's
output shows a wildcard subject.
Step 5 — Validate
- Check every workflow's YAML parses:
gh workflow list after pushing, or locally with python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" .github/workflows/<f>.yml for each file.
- Confirm every action reference is a current major version, and every secret/variable name used appears in Step
4's table — a typo'd secret name silently evaluates to empty, which usually surfaces as a confusing
authentication failure much later.
- Confirm the Maven commands actually work locally first (
mvn -B -ntp clean verify, mvn -B -ntp site -DskipTests -Djacoco.skip, mvn -B -ntp generate-resources) — delegate to iru-gate-runner for a compact
result. A workflow whose commands were never run locally is a guess.
- If the GraphQL step was added, run its commands locally too (
npm ci in apis/graphql-server/docs, then
spectaql and build-voyager.mjs) and open both pages in a browser. SpectaQL's output is server-rendered so
a wrong SDL path fails loudly, but Voyager renders client-side: a bad introspection payload or a missing
voyager.standalone.js gives a blank page with only a console error, and the workflow reports success.
- If the AsyncAPI step was added, run its commands locally too (
npm ci in apis/messaging/docs, then
asyncapi validate and asyncapi generate fromTemplate) and confirm an index.html is produced with the AVRO
field names in it. asyncapi validate exits 1 on a malformed document, a $ref to a missing .avsc, or an
.avsc that isn't valid AVRO — so it is a real gate, not decoration — while an out-of-date asyncapi: version
is only reported as information and exits 0, so a new specification release won't break the pipeline. Note that
npm ci here has failed before on an unpublished transitive dependency of the CLI, so a green run on your
machine with a warm cache isn't proof CI will install cleanly.
- Do not trigger
deploy.yml or undeploy.yml as a test. Recommend instead: a dry run of build.yml by
pushing to a throwaway branch with the trigger temporarily widened, and a first deploy.yml run against dev
only, watched to completion.
Step 6 — Report
Summarize: which workflow files were created or updated; the branch names and Java version resolved; whether the
Sonar step was included, omitted, or needs pom changes; whether the docs job's Antora install matches
docs/package.json (including Kroki); whether the AsyncAPI and GraphQL documentation steps were included and what
they publish under doc/api/messaging/ and doc/api/graphql-server/ (and whether the Voyager graph was part of
it); which environments deploy.yml/undeploy.yml offer and where they came
from; the container-image build approach chosen; the full secrets/variables/environments table from Step 4; and any
gap — missing infra/, missing Sonar config, missing GitHub Environments, a container stack too large for a
standard runner.
Warn explicitly:
- Review all three workflows before pushing.
deploy.yml and undeploy.yml change and destroy real
infrastructure; a wrong environment name or a missing approval gate has immediate consequences.
- Configure required reviewers on
prod before the workflows land on the main branch, not after — until then,
anyone with write access can dispatch a production deploy or teardown.
- Verify the OIDC trust policy is scoped to the repository and the environment.
- Integration tests need Docker on the runner; if the compose stack is too heavy for
ubuntu-latest, the first
build.yml run will fail on resource exhaustion rather than on a code defect — deal with it now if Step 0
flagged it.
- Nothing in these files contains a credential, and it must stay that way: every new value goes in as a secret or
variable reference.