# The pipeline, explained Anatomy of the generated Gitea Actions workflow (`assets/workflows/ci-cd.yaml`) — the jobs, the discovery loops, tag derivation, publish gating, and the `turbo prune` build model. Read this when editing the workflow or debugging a run. --- ## Jobs The workflow has two jobs; charts wait for images so a run doesn't publish a chart for an image that failed to build: ``` build-and-push-images ─▶ package-and-push-charts (needs: build-and-push-images) ``` Both begin the same way: `actions/checkout`, then a **login** step (§Harbor login), then a single **discover + act** shell step that loops over the convention paths. Why one shell step per job instead of an Actions `matrix`: discovery is dynamic (the set of apps changes as the monorepo grows) and a shell loop needs no round-trip to compute a matrix, no dependency on a marketplace action, and prints a clean per-item log. If you later want parallelism across apps, split discovery into a `prepare` job that emits a matrix via `GITHUB_OUTPUT` — but keep the loop as the default; it is the least fragile. --- ## Triggers and publish gating ```yaml on: push: branches: [test, main] tags: ["v*"] pull_request: branches: [test, main] ``` A single computed flag decides push vs. build-only. **Do not remove it** — a PR must prove the artifact builds without publishing anything: ```bash # PUBLISH=true only on a push to a release branch or a tag; never on a pull_request if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then PUBLISH=false elif [ "${GITHUB_REF_TYPE}" = "tag" ]; then PUBLISH=true elif [ "${GITHUB_REF_NAME}" = "test" ] || [ "${GITHUB_REF_NAME}" = "main" ]; then PUBLISH=true else PUBLISH=false fi ``` | Event | Images | Charts | |---|---|---| | `pull_request` | `docker build` (no push) | `helm lint` + `helm package` (no push) | | push to `test` / `main` | build **and** push `:` + `:latest` | package **and** push | | tag `v*` | build **and** push `:` | package **and** push (optionally `--version`/`--app-version` = tag) | --- ## Tagging ```bash if [ "${GITHUB_REF_TYPE}" = "tag" ]; then TAG="${GITHUB_REF_NAME}" # e.g. v1.4.2 else TAG="$(git rev-parse --short HEAD)" # e.g. 9f3a1c2 fi ``` - On the default branch, images are pushed with **both** `:` and a moving `:latest`. The immutable `:` is what GitOps should pin; `:latest` is a convenience, never a deploy source. - Chart version comes from `Chart.yaml`. On a tag build you may align it: `helm package --version "${TAG#v}" --app-version "${TAG#v}"`. Keep chart `version` (chart packaging) and `appVersion` (the image it deploys) conceptually distinct. - Never hardcode a tag in a Helm template or values file committed to the repo — the tag is a pipeline output that flows into GitOps values. See the monorepo skill's `containers-and-helm.md`. --- ## The `turbo prune` build model — why context is the repo root Each app's Dockerfile is **multi-stage** and runs `turbo prune --scope=@/ --docker` *inside* the build to isolate that app plus its workspace dependencies, then installs from the pruned `out/json/` before copying `out/full/`. That is what keeps image builds cacheable and independent across the monorepo. The consequence for the pipeline: the **build context must be the repository root**, and the Dockerfile is selected with `-f`: ```bash docker build -f "applications-backend//Dockerfile" -t "${IMAGE}:${TAG}" . # ^ context = repo root ``` Building with the app directory as context (`docker build applications-backend/`) fails — `turbo prune` and `COPY . .` cannot see the workspace or the lockfile. This is the single most common mistake; the template always builds from `.`. --- ## Discovery loops Images: ```bash shopt -s nullglob for df in applications-backend/*/Dockerfile applications-frontend/*/Dockerfile; do app="$(basename "$(dirname "$df")")" image="${HARBOR_ADDRESS}/${HARBOR_PROJECT}/${app}" docker build -f "$df" -t "${image}:${TAG}" . if [ "${PUBLISH}" = "true" ]; then docker push "${image}:${TAG}" if [ "${GITHUB_REF_TYPE}" != "tag" ]; then docker tag "${image}:${TAG}" "${image}:latest" && docker push "${image}:latest" fi fi done ``` Charts (OCI push to a `charts` subpath so charts and images don't collide in the project namespace): ```bash shopt -s nullglob mkdir -p .cicd-charts for chart in helm-charts/*/Chart.yaml; do dir="$(dirname "$chart")" helm lint "$dir" helm package "$dir" -d .cicd-charts done if [ "${PUBLISH}" = "true" ]; then for tgz in .cicd-charts/*.tgz; do helm push "$tgz" "oci://${HARBOR_ADDRESS}/${HARBOR_PROJECT}/charts" done fi ``` `nullglob` matters: without it, a repo with no frontend apps (or no charts) would iterate over the literal unexpanded glob and try to build a file named `applications-frontend/*/Dockerfile`. --- ## Harbor login step ```yaml - name: Log in to Harbor run: | echo "${HARBOR_SECRET}" | docker login "${HARBOR_ADDRESS}" -u "${HARBOR_USERNAME}" --password-stdin echo "${HARBOR_SECRET}" | helm registry login "${HARBOR_ADDRESS}" -u "${HARBOR_USERNAME}" --password-stdin env: HARBOR_ADDRESS: ${{ vars.HL_V1_HARBOR_ADDRESS }} HARBOR_USERNAME: ${{ vars.HL_V1_HARBOR_ROBOT_GITEA_ACTIONS_V1_USERNAME }} HARBOR_SECRET: ${{ secrets.HL_V1_HARBOR_ROBOT_GITEA_ACTIONS_V1_SECRET }} ``` On a `pull_request` the secret may be absent (org secrets are not exposed to fork PRs). The template guards the login: it only logs in when `PUBLISH=true`, so PR builds still verify without needing the credential. Base images pulled during the build must therefore be public, or the build itself must run on a trusted (non-fork) branch. --- ## `runs-on` and marketplace actions - `runs-on:` is a **runner label**, and each label maps to a container image. That image must provide `docker`, `helm`, and `git`. The template ships `runs-on: [self-hosted]` — change it to your registered label. - The only marketplace action used is `actions/checkout@v4`. Gitea proxies `actions/*` to github.com; if the runner has no egress, either mirror the action into your Gitea instance or replace checkout with a manual clone: ```yaml - run: | git init -q . git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git -c http.extraheader="AUTHORIZATION: bearer ${GITHUB_TOKEN}" fetch --depth=1 origin "${GITHUB_SHA}" git checkout -q FETCH_HEAD env: GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} ``` --- ## Verifying a change to the pipeline 1. Push to a throwaway feature branch → confirm the run **builds** images and **lints/packages** charts, and pushes **nothing** (`PUBLISH=false`). 2. Read the log for each app and chart — a silently skipped app means the glob didn't match (wrong directory, missing `Dockerfile`/`Chart.yaml`). 3. Merge to `test` → confirm `:` and `:latest` images and the chart appear in Harbor under the project. 4. Tag `v*` → confirm the tagged image and chart version.