CI pipelines
Translately’s GitHub Actions live under .github/workflows/. Six workflows handle PR validation, security scanning, link health, docs deploy, and signed-tag releases. Together with the master branch-protection rules, they form the full quality gate between a PR and a signed release tag.
Workflow map
flowchart LR
subgraph PR/Push
A[ci-backend.yml]
B[ci-webapp.yml]
C[codeql.yml]
D[link-checker.yml]
E[pages.yml]
end
subgraph Tag push
F[release.yml]
end
PR[Open/update PR] --> A
PR --> B
PR --> C
PR --> D
MASTER[Push to master] --> A
MASTER --> B
MASTER --> C
MASTER --> D
MASTER --> E
TAG[Push signed tag v*] --> F
F --> GHCR[ghcr.io images]
F --> REL[GitHub Release]
SCHED[Weekly schedule] -.-> C
SCHED -.-> D
Branch protection (master)
The workflows sit alongside branch-protection rules on master. Both together are the gate. Current settings (verify in repo settings or gh api repos/Pratiyush/translately/branches/master/protection):
- Required signed commits. Unsigned commits are rejected — no
--no-verify, ever. CLAUDE.md rule #1 is mirrored as server-side enforcement. - Required linear history. No merge commits on
master; PRs merge via squash or rebase. - Required CODEOWNERS review. Every matching path in
CODEOWNERSpulls@Pratiyushin as a required reviewer. - Dismiss stale reviews on new commits. Re-review required after any push.
- Required conversation resolution. Every review thread must be resolved before merge.
- No force-push, no branch deletion. Masters stays append-only.
CI workflows below are listed as required status checks — a red check blocks the merge button.
ci-backend.yml — backend build + test
File. Owns JVM build, lint, test, coverage for the Gradle multi-module backend.
Triggers: push to master and every pull_request, filtered to paths under backend/**, buildSrc/**, gradle/**, the top-level Gradle files, or this workflow itself. Unrelated docs-only PRs don’t spin it up.
Concurrency: ci-backend-$ with cancel-in-progress: true — a fresh push supersedes the previous run.
Permissions: contents: read.
Key steps:
actions/checkout@v4.actions/setup-java@v4— Temurin JDK 21.gradle/actions/setup-gradle@v4with cache cleanup../gradlew projects --no-daemon— sanity-check the 13-module wiring before spending time on anything heavier../gradlew ktlintCheck detekt --no-daemon --continue— ktlint + detekt (--continueso both lint reports emit even if one fails)../gradlew build --no-daemon --stacktrace— compile, test, Jacoco coverage, archive../gradlew jacocoTestReport --no-daemon(always) — regenerates the HTML report even when tests fail.- Upload
backend-coverageandbackend-test-reportsartifacts (14-day retention).
The checkOpenApiUpToDate Gradle task (wired into build via check in buildSrc/.../quarkus-app) fails the job if the committed docs/api/openapi.json drifts from what the backend emits — this is how API-spec docs stay in sync with code on every PR.
Required secrets: none.
ci-webapp.yml — webapp lint + test + build
File. Handles pnpm-based build for webapp/.
Triggers: push to master / any PR, filtered to webapp/**, pnpm-workspace.yaml, package.json, pnpm-lock.yaml, or this workflow file.
Concurrency: ci-webapp-$ with cancel-in-progress.
Guard job (guard): checks for webapp/package.json and outputs present=true|false. The build job is skipped when the webapp hasn’t been scaffolded yet — this was important in early Phase 0 before the webapp existed, and it’s still the defensive pattern if the tree is ever reshaped.
Build job key steps:
- Checkout.
pnpm/action-setup@v4+actions/setup-node@v4(Node 22, pnpm cache enabled).pnpm install --frozen-lockfile— lockfile drift fails the job.pnpm --filter @translately/webapp lint— ESLint + Prettier.pnpm --filter @translately/webapp codegen:check— regeneratessrc/lib/api/types.gen.tsfrom the committeddocs/api/openapi.jsonand fails if the output drifts. Mirrors the backend’scheckOpenApiUpToDateso the generated TypeScript client stays pinned to the spec.pnpm --filter @translately/webapp test -- --run— Vitest.pnpm --filter @translately/webapp build— Vite production bundle.- Upload
webapp-bundleartifact (webapp/dist/, 14-day retention).
Required secrets: none.
codeql.yml — SAST across three languages
File. GitHub CodeQL static analysis with the security-and-quality query pack.
Triggers:
pushtomaster.pull_requesttargetingmaster.- Scheduled: every Tuesday 03:17 UTC (catches new CodeQL rules without waiting for the next PR).
Permissions: actions: read, contents: read, security-events: write (so findings upload to the Security tab).
Detect job: probes for the presence of any JS/TS source root (webapp/, sdks/js/, sdks/react/, cli/). Sets has_jsts=true|false. CodeQL’s JS/TS extractor errors out with “no source” if it’s pointed at an empty tree, so we skip it until there’s something to scan.
Analyze matrix:
| Language | Build mode | Notes |
|---|---|---|
java-kotlin |
autobuild |
Uses the Gradle wrapper. Temurin JDK 21 set up before init. |
javascript-typescript |
none |
Gated on has_jsts=true. No build step needed — parses source directly. |
actions |
none |
Scans the workflow YAML itself for action misuse / pinning issues. |
Each matrix leg runs the standard codeql-action/init@v3 → codeql-action/analyze@v3 pair with queries: security-and-quality.
Timeout: 30 minutes. fail-fast: false so one language failing doesn’t mask the others.
Required secrets: none (uses the default GITHUB_TOKEN).
link-checker.yml — lychee
File. Runs lychee over every Markdown file and the built docs/**/*.html to catch link rot.
Triggers:
- Push / PR on
**/*.md, anything underdocs/, or this workflow file. - Scheduled: every Monday 06:13 UTC.
Config: lychee.toml at the repo root. Notable bits:
exclude_path = ["_reference"]— skip the gitignored third-party mirror.- A list of known-future URLs (release tags that 404 until their tag lands, the Pages site during first-deploy cache, the docs-bundle ZIP that’s built inside
pages.ymlrather than committed) is excluded so link-rot remains actionable. accept = [200, 202, 206, 301, 302, 303, 307, 308, 403, 429]— common redirect/throttle codes count as healthy.
Key steps:
- Checkout.
lycheeverse/lychee-action@v2with--config lychee.toml './**/*.md' 'docs/**/*.html',fail: true.- Always upload the
lychee-report.mdartifact (14-day retention).
Permissions: contents: read, issues: write (some lychee integrations can auto-open issues; we don’t currently wire that on).
Required secrets: none.
pages.yml — GitHub Pages deploy (docs site)
File. Builds the Jekyll site under docs/ with the just-the-docs remote theme and publishes it to GitHub Pages.
Triggers:
pushtomasterondocs/**or this workflow file.workflow_dispatch— manual re-deploy without a content change.
Permissions: contents: read, pages: write, id-token: write (required by actions/deploy-pages).
Concurrency: group: pages, cancel-in-progress: true — a newer deploy cancels an in-flight one.
Two jobs:
build(runs onubuntu-latest):- Checkout.
- Build the downloadable docs bundle:
docs/downloads/translately-docs.zipcontaining every raw.md/.txt/ image underdocs/so offline users can consume the full corpus without cloning. Built before Jekyll so it ends up inside_site. actions/configure-pages@v5.actions/jekyll-build-pages@v1withsource: ./docs,destination: ./_site.actions/upload-pages-artifact@v3.
deploy(depends onbuild):actions/deploy-pages@v4→ https://pratiyush.github.io/translately/.- Environment:
github-pages(enforces the Pages-specific OIDC token).
Required secrets: none — GitHub-managed Pages tokens only.
release.yml — signed-tag pipeline
File. The big one. Fires only when a v* tag is pushed.
Trigger: push with tags: ['v*']. Per the phase gate in CLAUDE.md rule #6, Phase N ends with a signed tag v0.N.0; v1.0.0 is Phase 7.
Permissions: contents: write (for the Release), packages: write (GHCR push), id-token: write (Cosign keyless).
Concurrency: release-$, not cancel-in-progress — releases run to completion.
Jobs:
verify — signed tag + version extraction
- Extracts
VERSION="${TAG#v}". - Flags
is_prerelease=truewhenVERSIONstarts with0.(all Phase 0–7 tags are pre-1.0). git tag -vchecks GPG signature. Currently a soft warning on CI (the ephemeral runner may not have the pubkey imported) — branch protection onmasteris the real enforcement that unsigned commits / tags can’t land.
build-backend
- Temurin JDK 21 + Gradle.
./gradlew :backend:app:build -x test -x checkOpenApiUpToDate --no-daemon— builds the Quarkus fast-jar, skippingtestandcheckOpenApiUpToDatebecause both depend on a boot-time Quarkus test-mode run that PR-time CI has already enforced.- Uploads
backend-fastjar-${VERSION}artifact (30-day retention).
build-webapp
- Guard-checks for
webapp/package.jsonfirst (mirrorsci-webapp.yml). - pnpm install +
pnpm --filter @translately/webapp build. - Uploads
webapp-bundle-${VERSION}artifact (30-day retention).
docker — multi-arch image build + push + sign
Depends on verify, build-backend, build-webapp. Skipped for 0.0.1 bootstraps (if: needs.verify.outputs.version != '0.0.1').
docker/setup-qemu-action@v3+docker/setup-buildx-action@v3.- Log in to
ghcr.iowith the job’sGITHUB_TOKEN. - Backend image:
docker/build-push-action@v6withinfra/docker/backend.Dockerfile, platformslinux/amd64,linux/arm64,provenance: mode=max,sbom: true. - Webapp image: same, conditional on
hashFiles('webapp/package.json') != ''. - Tags pushed:
ghcr.io/pratiyush/translately-{backend,webapp}:${VERSION}and:latest. - OCI labels pinned:
image.source,image.revision,image.version,image.licenses=MIT. sigstore/cosign-installer@v3+cosign sign(keyless,COSIGN_YES=true) — signatures verifiable from the image tag alone.
Required secrets: none (GHCR uses GITHUB_TOKEN; Cosign is OIDC/keyless via id-token: write).
release — GitHub Release creation
fetch-depth: 0so the CHANGELOG awk scrape can read history.- Extracts the
## [${VERSION}]section fromCHANGELOG.mdwithawk; falls back to a stub. - Downloads the backend + webapp artifacts and
tar -czf’s them intotranslately-backend-${VERSION}.tar.gz/translately-webapp-${VERSION}.tar.gz. softprops/action-gh-release@v2creates or updates the Release, attaches both tarballs, and flags pre-release based on theverifyoutput.
How the workflows compose
- Every PR runs
ci-backend,ci-webapp(when webapp paths changed),codeql, andlink-checker(for Markdown/docs changes). All four must be green; CODEOWNERS approval + signed commits are required; linear history is enforced on merge. - Push to
masterre-runs the same four and additionally deploysdocs/viapages.ymlwhen the docs tree changes. - Signed
v*tag push triggersrelease.yml— builds fast-jar + webapp bundle, publishes signed multi-arch images to GHCR, and creates the GitHub Release with CHANGELOG-sourced notes. - Scheduled runs — CodeQL weekly (Tue 03:17 UTC) catches new rule updates; lychee weekly (Mon 06:13 UTC) catches external link rot that a code-free week would miss.
For the contributor-facing workflow that sits on top of these pipelines (branch naming, commit conventions, pre-merge checklist), see .kiro/steering/contributing-rules.md and the pull-request template.