diff --git a/.github/prompts/renovate-major-review.md b/.github/prompts/renovate-major-review.md index bac6aed0..2c6edf42 100644 --- a/.github/prompts/renovate-major-review.md +++ b/.github/prompts/renovate-major-review.md @@ -7,7 +7,7 @@ You only read. Never modify files, push, approve, label, or comment yourself. ## How this repository deploys - Helm charts are never installed with `helm install` / `helm upgrade`. - `.github/scripts/kubernetes-hydrate/hydrate-component.sh` renders `kubernetes/components///` with `helmfile template --include-crds` (plus `kustomize build kustomization/`) into `kubernetes/manifests///manifest.yaml`, and Flux applies them after merge (production only: the `flux-system` Kustomization syncs `kubernetes/clusters/production`, which includes `kubernetes/manifests/production`, with `prune: true`). + `scripts/kubernetes-hydrate/hydrate-component.sh` of `panicboat/.github` renders `kubernetes/components///` with `helmfile template --include-crds` (plus `kustomize build kustomization/`) into `kubernetes/manifests///manifest.yaml`, and Flux applies them after merge (production only: the `flux-system` Kustomization syncs `kubernetes/clusters/production`, which includes `kubernetes/manifests/production`, with `prune: true`). - Consequences for upgrade guides written for `helm upgrade`: - CRDs rendered from the chart with this repository's values are applied by Flux with everything else, so "apply the CRDs manually before `helm upgrade`" steps are already satisfied for them; do not report them as manual work. CRDs the guide expects from another source (e.g. Gateway API CRDs managed by `kubernetes/components/gateway-api`) are not covered; report the needed change to that component as ⚠️. @@ -27,7 +27,7 @@ You only read. Never modify files, push, approve, label, or comment yourself. - the files Renovate changed 3. Ignore changes under `kubernetes/manifests/`; the hydrator generates them in parallel with this review, so they may be stale. 4. Read `.render-diff/summary.md` (the rendered diff). - For every component directory the pull request changes, it compares the base and the head rendered with this repository's values (`.github/scripts/kubernetes-hydrate/render-diff.sh`): image references, CRD versions, and added / removed resources. + For every component directory the pull request changes, it compares the base and the head rendered with this repository's values (`scripts/kubernetes-hydrate/render-diff.sh` of `panicboat/.github`): image references, CRD versions, and added / removed resources. Its `Full diff` paths are relative to `.render-diff/`; the files can be large, so Grep them instead of reading them whole. A component marked `Rendering failed` or `Summarizing failed` is uncovered (for `Summarizing failed`, still Grep its full diff). A component marked `Not rendered` is not deployed by the hydrator; review it from its files only. diff --git a/.github/renovate.json b/.github/renovate.json index 2ccd1180..c3fc610e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,71 +1,9 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:recommended", - ":semanticCommits", - ":separateMajorReleases", - "group:monorepos", - "group:recommended", - "helpers:pinGitHubActionDigests" + "github>panicboat/.github" ], - "semanticCommitScope": "{{packageFileDir}}", - "timezone": "Asia/Tokyo", - "labels": [ - "🤖 renovate" - ], - "assigneesFromCodeOwners": false, - "reviewersFromCodeOwners": true, - "automergeType": "pr", - "automergeStrategy": "squash", - "platformAutomerge": true, - "prConcurrentLimit": 15, - "prHourlyLimit": 5, - "branchPrefix": "renovate/", - "rangeStrategy": "bump", - "schedule": [ - "before 4am every weekday" - ], - "minimumReleaseAge": "1 day", - "separateMinorPatch": false, "packageRules": [ - { - "description": "Default: separate PRs per package", - "groupName": "{{packageFileDir}}/{{depName}}", - "matchPackageNames": [ - "*" - ] - }, - { - "description": "🚫 Production environments - disable automerge", - "matchFileNames": [ - "**/production/**" - ], - "automerge": false, - "prPriority": -1, - "addLabels": [ - "⚠️ production" - ] - }, - { - "description": "🚫 Major updates - disable automerge", - "matchUpdateTypes": [ - "major" - ], - "automerge": false, - "addLabels": [ - "📈 major-update" - ] - }, - { - "description": "✅ Minor/Patch updates - enable automerge", - "matchUpdateTypes": [ - "minor", - "patch", - "pin", - "digest" - ], - "automerge": true - }, { "description": "Track OpenTofu releases instead of Terraform for required_version", "matchDepTypes": [ diff --git a/.github/scripts/kubernetes-hydrate/hydrate-component.sh b/.github/scripts/kubernetes-hydrate/hydrate-component.sh deleted file mode 100755 index 13034217..00000000 --- a/.github/scripts/kubernetes-hydrate/hydrate-component.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -component="${1:?component name required}" -env="${2:?environment name required}" - -cd "$(git rev-parse --show-toplevel)" - -component_dir="kubernetes/components/${component}/${env}" -out_dir="kubernetes/manifests/${env}/${component}" - -# Explicit --kube-version prevents template divergence caused by varying default helm binary versions. -env_hcl="aws/eks/${env}/env.hcl" -if [ ! -f "${env_hcl}" ]; then - echo "hydrate-component.sh: ${env_hcl} not found; cannot determine cluster version" >&2 - exit 1 -fi -kube_version=$(sed -n 's/^[[:space:]]*cluster_version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "${env_hcl}") -if [ -z "${kube_version}" ]; then - echo "hydrate-component.sh: cluster_version not set in ${env_hcl}" >&2 - exit 1 -fi - -mkdir -p "${out_dir}" -: > "${out_dir}/manifest.yaml" - -if [ -f "${component_dir}/helmfile.yaml" ]; then - helmfile -f "${component_dir}/helmfile.yaml" -e "${env}" template \ - --include-crds --skip-tests --kube-version "${kube_version}" >> "${out_dir}/manifest.yaml" -fi - -if [ -d "${component_dir}/kustomization" ]; then - echo "---" >> "${out_dir}/manifest.yaml" - kustomize build "${component_dir}/kustomization" >> "${out_dir}/manifest.yaml" -fi - -printf "resources:\n - manifest.yaml\n" > "${out_dir}/kustomization.yaml" - -if git ls-files --error-unmatch "${out_dir}/manifest.yaml" >/dev/null 2>&1; then - if git diff --quiet -I '^[[:space:]]*(ca\.crt|ca\.key|tls\.crt|tls\.key|caBundle):' -- "${out_dir}/manifest.yaml"; then - git checkout -- "${out_dir}/manifest.yaml" - fi -fi diff --git a/.github/scripts/kubernetes-hydrate/hydrate-index.sh b/.github/scripts/kubernetes-hydrate/hydrate-index.sh deleted file mode 100755 index 291dbd42..00000000 --- a/.github/scripts/kubernetes-hydrate/hydrate-index.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -env="${1:?environment name required}" - -cd "$(git rev-parse --show-toplevel)" - -env_dir="kubernetes/manifests/${env}" - -mkdir -p "${env_dir}/00-namespaces" -: > "${env_dir}/00-namespaces/namespaces.yaml" - -for comp_dir in kubernetes/components/*/"${env}"/; do - [ -d "${comp_dir}" ] || continue - comp_name=$(basename "$(dirname "${comp_dir}")") - if [ -f "kubernetes/components/${comp_name}/${env}/namespace.yaml" ]; then - echo "---" >> "${env_dir}/00-namespaces/namespaces.yaml" - cat "kubernetes/components/${comp_name}/${env}/namespace.yaml" >> "${env_dir}/00-namespaces/namespaces.yaml" - elif [ -f "kubernetes/components/${comp_name}/namespace.yaml" ]; then - echo "---" >> "${env_dir}/00-namespaces/namespaces.yaml" - cat "kubernetes/components/${comp_name}/namespace.yaml" >> "${env_dir}/00-namespaces/namespaces.yaml" - fi -done - -printf "resources:\n - namespaces.yaml\n" > "${env_dir}/00-namespaces/kustomization.yaml" - -for dir in "${env_dir}"/*/; do - [ -d "${dir}" ] || continue - name=$(basename "${dir}") - if [ "${name}" = "00-namespaces" ]; then - continue - fi - if [ ! -d "kubernetes/components/${name}/${env}" ]; then - rm -rf "${dir}" - fi -done - -{ - echo "resources:" - echo " - ./00-namespaces" - # Sort by full path in C locale so prefix pairs like opentelemetry-collector precede opentelemetry. - for dir in "${env_dir}"/*/; do - [ -d "${dir}" ] || continue - name=$(basename "${dir}") - [ "${name}" = "00-namespaces" ] && continue - printf '%s\n' "${dir}" - done | LC_ALL=C sort | while IFS= read -r dir; do - echo " - ./$(basename "${dir}")" - done -} > "${env_dir}/kustomization.yaml" diff --git a/.github/scripts/kubernetes-hydrate/render-diff.sh b/.github/scripts/kubernetes-hydrate/render-diff.sh deleted file mode 100755 index 1446774f..00000000 --- a/.github/scripts/kubernetes-hydrate/render-diff.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -base_ref="${1:?base revision required}" -out_dir="${2:?output directory required}" - -cd "$(git rev-parse --show-toplevel)" -script_dir="$(pwd)/.github/scripts/kubernetes-hydrate" -mkdir -p "${out_dir}" -out_dir="$(cd "${out_dir}" && pwd)" -summary="${out_dir}/summary.md" - -components="$(git diff --name-only --no-renames "${base_ref}" HEAD -- kubernetes/components/ | awk -F/ 'NF >= 5 { print $3 "/" $4 }' | sort -u)" -if [ -z "${components}" ]; then - echo "No Kubernetes components changed." > "${summary}" - exit 0 -fi - -work="$(mktemp -d)" -cleanup() { - git worktree remove --force "${work}/base" 2>/dev/null || true # SILENT: the worktree may not exist when setup failed - git worktree remove --force "${work}/head" 2>/dev/null || true # SILENT: the worktree may not exist when setup failed - rm -rf "${work}" -} -trap cleanup EXIT -# Separate worktrees keep hydrate-component.sh from rewriting kubernetes/manifests in this checkout. -git worktree add -q --detach "${work}/base" "${base_ref}" -git worktree add -q --detach "${work}/head" HEAD - -render() { - local side="$1" svc="$2" env="$3" dir="$4" - (cd "${work}/${side}" && bash "${script_dir}/hydrate-component.sh" "${svc}" "${env}") > "${dir}/${side}.log" 2>&1 && - cp "${work}/${side}/kubernetes/manifests/${env}/${svc}/manifest.yaml" "${dir}/${side}.yaml" -} - -: > "${summary}" -for component in ${components}; do - svc="${component%/*}" - env="${component#*/}" - dir="${out_dir}/${env}/${svc}" - { - echo "## ${svc} (${env})" - echo - if [ ! -f "aws/eks/${env}/env.hcl" ]; then - echo "Not rendered: \`${env}\` has no \`aws/eks/${env}/env.hcl\`, so the hydrator never deploys it." - echo - continue - fi - mkdir -p "${dir}" - failed="" - render base "${svc}" "${env}" "${dir}" || failed="base" - render head "${svc}" "${env}" "${dir}" || failed="${failed:+${failed} }head" - if [ -n "${failed}" ]; then - echo "Rendering failed (${failed}):" - echo - echo '```' - for side in ${failed}; do - tail -n 20 "${dir}/${side}.log" - done - echo '```' - else - diff -u "${dir}/base.yaml" "${dir}/head.yaml" > "${dir}/manifest.diff" || true # SILENT: diff exits 1 when the manifests differ - echo "Full diff: \`${env}/${svc}/manifest.diff\`" - echo - if bash "${script_dir}/summarize-manifest-diff.sh" "${dir}/base.yaml" "${dir}/head.yaml" > "${dir}/summary.part" 2> "${dir}/summarize.log"; then - cat "${dir}/summary.part" - else - echo "Summarizing failed; use the full diff:" - echo - echo '```' - tail -n 20 "${dir}/summarize.log" - echo '```' - fi - fi - echo - } >> "${summary}" -done diff --git a/.github/scripts/kubernetes-hydrate/summarize-manifest-diff.sh b/.github/scripts/kubernetes-hydrate/summarize-manifest-diff.sh deleted file mode 100755 index c3f23faa..00000000 --- a/.github/scripts/kubernetes-hydrate/summarize-manifest-diff.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -base="${1:?base manifest required}" -head="${2:?head manifest required}" - -export LC_ALL=C - -image_refs() { - { - yq -N '.. | select(tag == "!!map" and has("image")) | .image | select(tag == "!!str")' "$1" - yq -N '.. | select(tag == "!!str") | select(test("^(--[A-Za-z0-9-]+=)?[a-z0-9.-]+(:[0-9]+)?/[a-z0-9./_-]+:[A-Za-z0-9._-]+(@sha256:[a-f0-9]+)?$")) | sub("^--[A-Za-z0-9-]+=", "")' "$1" - } | sort -u -} - -split_image_refs() { - awk -v side="$1" '{ - ref = $0; digest = ""; tag = ""; repo = ref - at = index(ref, "@") - if (at > 0) { digest = substr(ref, at); ref = substr(ref, 1, at - 1); repo = ref } - n = split(ref, parts, "/"); last = parts[n] - colon = index(last, ":") - if (colon > 0) { tag = substr(last, colon + 1); repo = substr(ref, 1, length(ref) - length(last) + colon - 1) } - tag = tag digest - if (tag == "") tag = "(untagged)" - print repo "\t" side "\t" tag - }' -} - -crd_versions() { - yq -N 'select(.kind == "CustomResourceDefinition") | .metadata.name + "\t" + ([.spec.versions[] | .name + (if .served == false then " (not served)" else "" end) + (if .storage then "*" else "" end)] | join(", "))' "$1" | sort -u -} - -resources() { - yq -N 'select(.kind != null) | .kind + " " + (if .metadata.namespace then .metadata.namespace + "/" else "" end) + .metadata.name' "$1" | sort -u -} - -pair_rows() { - awk -F '\t' '{ - key[$1] = 1 - if ($2 == "base") base[$1] = (base[$1] == "" ? $3 : base[$1] ", " $3) - else head[$1] = (head[$1] == "" ? $3 : head[$1] ", " $3) - } - END { - for (k in key) if (base[k] != head[k]) print k "\t" (base[k] == "" ? "-" : base[k]) "\t" (head[k] == "" ? "-" : head[k]) - }' | sort -} - -print_table() { - local header="$1" rows="$2" - if [ -z "${rows}" ]; then - echo "No changes." - return - fi - echo "| ${header} | Base | Head |" - echo "|---|---|---|" - printf '%s\n' "${rows}" | awk -F '\t' '{ print "| " $1 " | " $2 " | " $3 " |" }' -} - -tmp="$(mktemp -d)" -trap 'rm -rf "${tmp}"' EXIT -# Extracting into files first lets set -e catch yq failures that command substitutions and process substitutions would hide. -for side in base head; do - if [ "${side}" = base ]; then manifest="${base}"; else manifest="${head}"; fi - image_refs "${manifest}" | split_image_refs "${side}" > "${tmp}/${side}.images" - crd_versions "${manifest}" | awk -F '\t' -v side="${side}" '{ print $1 "\t" side "\t" $2 }' > "${tmp}/${side}.crds" - resources "${manifest}" > "${tmp}/${side}.resources" -done - -image_rows="$(cat "${tmp}/base.images" "${tmp}/head.images" | pair_rows)" -crd_rows="$(cat "${tmp}/base.crds" "${tmp}/head.crds" | pair_rows)" -added="$(comm -13 "${tmp}/base.resources" "${tmp}/head.resources")" -removed="$(comm -23 "${tmp}/base.resources" "${tmp}/head.resources")" - -echo "### Images" -echo -if [ -n "${image_rows}" ]; then - echo 'Collected from `image` fields and from other string values shaped like `registry/repository:tag` (args, env).' - echo -fi -print_table "Repository" "${image_rows}" -echo -echo "### CustomResourceDefinitions" -echo -if [ -n "${crd_rows}" ]; then - echo '`*` marks the storage version.' - echo -fi -print_table "Name" "${crd_rows}" -echo -echo "### Resources" -echo -if [ -z "${added}" ] && [ -z "${removed}" ]; then - echo "No changes." -fi -if [ -n "${added}" ]; then - echo "Added:" - printf '%s\n' "${added}" | sed 's/^/- /' -fi -if [ -n "${added}" ] && [ -n "${removed}" ]; then - echo -fi -if [ -n "${removed}" ]; then - echo "Removed:" - printf '%s\n' "${removed}" | sed 's/^/- /' -fi diff --git a/.github/scripts/kubernetes-hydrate/tests/fixtures/identical/expected.md b/.github/scripts/kubernetes-hydrate/tests/fixtures/identical/expected.md deleted file mode 100644 index 73262562..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/fixtures/identical/expected.md +++ /dev/null @@ -1,11 +0,0 @@ -### Images - -No changes. - -### CustomResourceDefinitions - -No changes. - -### Resources - -No changes. diff --git a/.github/scripts/kubernetes-hydrate/tests/fixtures/identical/manifest.yaml b/.github/scripts/kubernetes-hydrate/tests/fixtures/identical/manifest.yaml deleted file mode 100644 index c05d945e..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/fixtures/identical/manifest.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: same diff --git a/.github/scripts/kubernetes-hydrate/tests/fixtures/invalid/manifest.yaml b/.github/scripts/kubernetes-hydrate/tests/fixtures/invalid/manifest.yaml deleted file mode 100644 index c2b6f361..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/fixtures/invalid/manifest.yaml +++ /dev/null @@ -1,2 +0,0 @@ -kind: ConfigMap -metadata: [ diff --git a/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/base.yaml b/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/base.yaml deleted file mode 100644 index 69182fe2..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/base.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: widgets.example.com -spec: - versions: - - name: v1alpha1 - served: true - storage: true ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: gadgets.example.com -spec: - versions: - - name: v1 - served: true - storage: true ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: app - namespace: ns-a -spec: - template: - spec: - initContainers: - - name: init - image: busybox:1.36 - containers: - - name: app - image: registry.example.com/app:1.0 - args: - - --config-reloader=quay.io/example/reloader:v0.93.1 - - --log-level=info - env: - - name: HELPER_IMAGE - value: ghcr.io/example/helper:2.0 - - name: ENDPOINT - value: http://example.com/path:8080 ---- -apiVersion: monitoring.coreos.com/v1 -kind: Prometheus -metadata: - name: main - namespace: ns-a -spec: - image: quay.io/prometheus/prometheus:v3.14.0 - version: v3.14.0 ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: old - namespace: ns-a ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: reader diff --git a/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/expected.md b/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/expected.md deleted file mode 100644 index d46e01ca..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/expected.md +++ /dev/null @@ -1,29 +0,0 @@ -### Images - -Collected from `image` fields and from other string values shaped like `registry/repository:tag` (args, env). - -| Repository | Base | Head | -|---|---|---| -| localhost:5000/sidecar | - | 0.1@sha256:abc | -| nginx | - | (untagged) | -| quay.io/example/reloader | v0.93.1 | v0.94.1 | -| quay.io/prometheus/prometheus | v3.14.0 | v3.15.0 | -| registry.example.com/app | 1.0 | 2.0 | - -### CustomResourceDefinitions - -`*` marks the storage version. - -| Name | Base | Head | -|---|---|---| -| gadgets.example.com | v1* | - | -| widgets.example.com | v1alpha1* | v1alpha1 (not served), v1* | - -### Resources - -Added: -- ConfigMap ns-a/new - -Removed: -- ConfigMap ns-a/old -- CustomResourceDefinition gadgets.example.com diff --git a/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/head.yaml b/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/head.yaml deleted file mode 100644 index 07a34d59..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/fixtures/summarize/head.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: widgets.example.com -spec: - versions: - - name: v1alpha1 - served: false - storage: false - - name: v1 - served: true - storage: true ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: app - namespace: ns-a -spec: - template: - spec: - initContainers: - - name: init - image: busybox:1.36 - containers: - - name: app - image: registry.example.com/app:2.0 - args: - - --config-reloader=quay.io/example/reloader:v0.94.1 - - --log-level=info - env: - - name: HELPER_IMAGE - value: ghcr.io/example/helper:2.0 - - name: ENDPOINT - value: http://example.com/path:8080 - - name: sidecar - image: localhost:5000/sidecar:0.1@sha256:abc - - name: untagged - image: nginx ---- ---- -apiVersion: monitoring.coreos.com/v1 -kind: Prometheus -metadata: - name: main - namespace: ns-a -spec: - image: quay.io/prometheus/prometheus:v3.15.0 - version: v3.15.0 - thanos: - image: - repository: quay.io/thanos/thanos ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: new - namespace: ns-a ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: reader diff --git a/.github/scripts/kubernetes-hydrate/tests/render-diff.test.sh b/.github/scripts/kubernetes-hydrate/tests/render-diff.test.sh deleted file mode 100755 index 04a9650e..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/render-diff.test.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -here="$(cd "$(dirname "$0")" && pwd)" -scripts="${here}/.." -work="$(mktemp -d)" -trap 'rm -rf "${work}"' EXIT -failures=0 - -pass() { echo "ok - $1"; } -fail() { echo "not ok - $1"; failures=$((failures + 1)); } -expect_contains() { if grep -qF -- "$2" "$3"; then pass "$1"; else fail "$1"; sed 's/^/# /' "$3"; fi; } -expect_absent() { if grep -qF -- "$2" "$3"; then fail "$1"; sed 's/^/# /' "$3"; else pass "$1"; fi; } - -write_deployment() { - local dir="$1" name="$2" image="$3" - mkdir -p "${dir}/kustomization" - printf 'resources:\n - deployment.yaml\n' > "${dir}/kustomization/kustomization.yaml" - cat > "${dir}/kustomization/deployment.yaml" < "${repo}/aws/eks/production/env.hcl" -write_deployment "${repo}/kubernetes/components/demo/production" demo nginx:1.0 -write_deployment "${repo}/kubernetes/components/gone/production" gone busybox:1.36 -write_deployment "${repo}/kubernetes/components/untouched/production" untouched redis:7 -touch "${repo}/kubernetes/components/demo/namespace.yaml" -git -C "${repo}" init -q -b main -git -C "${repo}" -c user.name=test -c user.email=test@example.com add -A -git -C "${repo}" -c user.name=test -c user.email=test@example.com commit -q -m base - -write_deployment "${repo}/kubernetes/components/demo/production" demo nginx:1.1 -echo "# changed" >> "${repo}/kubernetes/components/demo/namespace.yaml" -git -C "${repo}" rm -q -r kubernetes/components/gone -write_deployment "${repo}/kubernetes/components/pending-only/pending" pending-only busybox:1.36 -write_deployment "${repo}/kubernetes/components/broken/production" broken busybox:1.36 -printf 'resources:\n - missing.yaml\n' > "${repo}/kubernetes/components/broken/production/kustomization/kustomization.yaml" -git -C "${repo}" -c user.name=test -c user.email=test@example.com add -A -git -C "${repo}" -c user.name=test -c user.email=test@example.com commit -q -m head - -out="${work}/out" -(cd "${repo}" && bash .github/scripts/kubernetes-hydrate/render-diff.sh HEAD^ "${out}") > "${work}/stdout" 2>&1 || { fail "render-diff exits 0"; sed 's/^/# /' "${work}/stdout"; } -summary="${out}/summary.md" - -expect_contains "lists a changed component" "## demo (production)" "${summary}" -expect_contains "reports the changed image" "| nginx | 1.0 | 1.1 |" "${summary}" -expect_contains "links the full diff" "production/demo/manifest.diff" "${summary}" -expect_contains "writes the full diff" "+ - image: nginx:1.1" "${out}/production/demo/manifest.diff" -expect_contains "lists a deleted component" "## gone (production)" "${summary}" -expect_contains "reports resources of a deleted component as removed" "- Deployment gone" "${summary}" -expect_contains "reports a render failure instead of aborting" "Rendering failed (head)" "${summary}" -expect_contains "skips directories that are not hydrated environments" "Not rendered:" "${summary}" -if [ -d "${out}/pending" ]; then fail "does not render directories that are not hydrated environments"; else pass "does not render directories that are not hydrated environments"; fi -expect_absent "skips components without changes" "untouched" "${summary}" -if [ -z "$(git -C "${repo}" status --porcelain)" ] && [ "$(git -C "${repo}" worktree list | wc -l)" -eq 1 ]; then - pass "leaves the checkout and worktree list clean" -else - fail "leaves the checkout and worktree list clean" - git -C "${repo}" status --porcelain | sed 's/^/# /' -fi - -failing_bin="${work}/failing-bin" -mkdir -p "${failing_bin}" -printf '#!/bin/sh\nexit 1\n' > "${failing_bin}/yq" -chmod +x "${failing_bin}/yq" -(cd "${repo}" && PATH="${failing_bin}:${PATH}" bash .github/scripts/kubernetes-hydrate/render-diff.sh HEAD^ "${work}/out-failing") > /dev/null 2>&1 || fail "render-diff exits 0 when summarizing fails" -expect_contains "reports a summarizing failure instead of aborting" "Summarizing failed" "${work}/out-failing/summary.md" - -echo "# changed" >> "${repo}/README.md" -git -C "${repo}" -c user.name=test -c user.email=test@example.com add -A -git -C "${repo}" -c user.name=test -c user.email=test@example.com commit -q -m docs -(cd "${repo}" && bash .github/scripts/kubernetes-hydrate/render-diff.sh HEAD^ "${work}/out-none") > /dev/null 2>&1 || fail "render-diff exits 0 without components" -expect_contains "reports when no component changed" "No Kubernetes components changed." "${work}/out-none/summary.md" - -exit "${failures}" diff --git a/.github/scripts/kubernetes-hydrate/tests/summarize-manifest-diff.test.sh b/.github/scripts/kubernetes-hydrate/tests/summarize-manifest-diff.test.sh deleted file mode 100755 index 1d7d7c0e..00000000 --- a/.github/scripts/kubernetes-hydrate/tests/summarize-manifest-diff.test.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -here="$(cd "$(dirname "$0")" && pwd)" -summarize="${here}/../summarize-manifest-diff.sh" -fixtures="${here}/fixtures" -failures=0 - -check() { - local name="$1" base="$2" head="$3" expected="$4" actual - if actual="$(bash "${summarize}" "${base}" "${head}" 2>&1)" && diff -u "${expected}" <(printf '%s\n' "${actual}"); then - echo "ok - ${name}" - else - echo "not ok - ${name}" - printf '%s\n' "${actual}" | sed 's/^/# /' - failures=$((failures + 1)) - fi -} - -check "reports image, CRD and resource changes" \ - "${fixtures}/summarize/base.yaml" "${fixtures}/summarize/head.yaml" "${fixtures}/summarize/expected.md" -check "reports no changes for identical manifests" \ - "${fixtures}/identical/manifest.yaml" "${fixtures}/identical/manifest.yaml" "${fixtures}/identical/expected.md" - -for side in base head; do - if [ "${side}" = base ]; then args=("${fixtures}/invalid/manifest.yaml" "${fixtures}/identical/manifest.yaml"); else args=("${fixtures}/identical/manifest.yaml" "${fixtures}/invalid/manifest.yaml"); fi - if bash "${summarize}" "${args[@]}" > /dev/null 2>&1; then - echo "not ok - fails on an unparsable ${side} manifest" - failures=$((failures + 1)) - else - echo "ok - fails on an unparsable ${side} manifest" - fi -done - -exit "${failures}" diff --git a/.github/scripts/terragrunt-release/check-release-configs.sh b/.github/scripts/terragrunt-release/check-release-configs.sh deleted file mode 100755 index 0d669b6e..00000000 --- a/.github/scripts/terragrunt-release/check-release-configs.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -config_path="${1:-workflow-config.yaml}" - -# release-targets.sh stderr is hidden below, so an unreadable config would otherwise look like "no release services" -yq '.' "${config_path}" >/dev/null - -merge_environments="$(yq -o=json '.' "${config_path}" | jq -c ' - [.stacks[] | select(.name == "terragrunt") | .environments | to_entries[] | select(.value.apply_trigger == "merge") | .key] -')" - -failed=0 - -report() { - echo "::error::$*" >&2 - failed=1 -} - -shopt -s nullglob -for service_dir in aws/*/; do - service="$(basename "${service_dir}")" - package="aws/${service}" - config_file="${package}/release-please-config.json" - manifest_file="${package}/release-please-manifest.json" - - if ! targets="$(bash "${script_dir}/release-targets.sh" "${service}" "${config_path}" 2>/dev/null)"; then - if [ -f "${config_file}" ]; then - report "${config_file} exists but ${service} has no release environment" - fi - continue - fi - - incomplete="$(jq -r ' - .[] | . as $target - | ("aws_region", "iam_role_plan", "iam_role_apply") as $field - | select(($target[$field] // "") == "") - | "::error::release target \($target.service)/\($target.environment) has no \($field)" - ' <<<"${targets}")" - if [ -n "${incomplete}" ]; then - echo "${incomplete}" >&2 - failed=1 - fi - - if [ ! -f "${config_file}" ]; then - report "${config_file} is missing but ${service} has a release environment" - continue - fi - - component="$(jq -r --arg package "${package}" '.packages[$package].component // ""' "${config_file}")" - if [ "${component}" != "${service}" ]; then - report "${package}: component must be ${service} (got '${component}')" - fi - - expected_exclude_paths="$(jq -c --arg package "${package}" 'map("\($package)/\(.)")' <<<"${merge_environments}")" - actual_exclude_paths="$(jq -c --arg package "${package}" '.packages[$package]["exclude-paths"]' "${config_file}")" - if ! jq -ne --argjson expected "${expected_exclude_paths}" --argjson actual "${actual_exclude_paths}" ' - ($actual | type) == "array" and ($actual | sort) == ($expected | sort) - ' >/dev/null; then - report "${package}: exclude-paths must be ${expected_exclude_paths} (got ${actual_exclude_paths})" - fi - - if [ ! -f "${manifest_file}" ] || ! jq -e --arg package "${package}" 'has($package)' "${manifest_file}" >/dev/null; then - report "${manifest_file} must have the key ${package}" - fi -done - -exit "${failed}" diff --git a/.github/scripts/terragrunt-release/release-service.sh b/.github/scripts/terragrunt-release/release-service.sh deleted file mode 100755 index 27ebbb99..00000000 --- a/.github/scripts/terragrunt-release/release-service.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -mode="${1:-}" -value="${2:-}" - -case "${mode}" in - --tag) - pattern='^([a-z0-9-]+)-v[0-9]+\.[0-9]+\.[0-9]+$' - expected='-vX.Y.Z' - ;; - --branch) - pattern='^release-please--branches--main--components--([a-z0-9-]+)$' - expected='release-please--branches--main--components--' - ;; - *) - echo "::error::usage: release-service.sh --tag | --branch " >&2 - exit 1 - ;; -esac - -if [[ ! "${value}" =~ ${pattern} ]]; then - shown="${value//$'\n'/ }" - shown="${shown//$'\r'/ }" - echo "::error::'${shown}' does not match ${expected}" >&2 - exit 1 -fi - -echo "${BASH_REMATCH[1]}" diff --git a/.github/scripts/terragrunt-release/release-targets.sh b/.github/scripts/terragrunt-release/release-targets.sh deleted file mode 100755 index 8cc1aa50..00000000 --- a/.github/scripts/terragrunt-release/release-targets.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -service="${1:?usage: release-targets.sh [config_path]}" -config_path="${2:-workflow-config.yaml}" - -candidates="$(yq -o=json '.' "${config_path}" | jq -c --arg service "${service}" ' - .stacks[] - | select(.name == "terragrunt") - | (.exclude // []) as $exclude - | .environments | to_entries[] - | select(.value.apply_trigger == "release") - | {service: $service, environment: .key} as $values - | { - service: $service, - environment: .key, - working_directory: "aws/\($service)/\(.key)", - aws_region: .value.aws_region, - iam_role_plan: .value.iam_role_plan, - iam_role_apply: .value.iam_role_apply, - excluded: any($exclude[]; to_entries | all(.[]; . as $pair | $values | has($pair.key) and .[$pair.key] == $pair.value)) - } -')" - -targets="$( - while IFS= read -r candidate; do - [ -n "${candidate}" ] || continue - if [ -d "$(jq -r '.working_directory' <<<"${candidate}")" ]; then - echo "${candidate}" - fi - done <<<"${candidates}" | jq -c -s '.' -)" - -if [ "$(jq 'length' <<<"${targets}")" -eq 0 ]; then - echo "::error::no release targets for ${service}" >&2 - exit 1 -fi - -jq -c 'map(select(.excluded | not) | del(.excluded))' <<<"${targets}" diff --git a/.github/scripts/terragrunt-release/test.sh b/.github/scripts/terragrunt-release/test.sh deleted file mode 100755 index 59bb7f4e..00000000 --- a/.github/scripts/terragrunt-release/test.sh +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)" -work_root="$(mktemp -d)" -trap 'rm -rf "${work_root}"' EXIT - -last_stderr="" - -assert_eq() { - local expected="$1" actual="$2" - if [ "${expected}" != "${actual}" ]; then - echo " expected: ${expected}" >&2 - echo " actual: ${actual}" >&2 - exit 1 - fi -} - -assert_fails() { - local stderr_file="${work_root}/stderr" - if "$@" 2>"${stderr_file}"; then - echo " expected a non-zero exit: $*" >&2 - exit 1 - fi - last_stderr="$(cat "${stderr_file}")" -} - -assert_stderr_contains() { - local needle="$1" - if [[ "${last_stderr}" != *"${needle}"* ]]; then - echo " stderr does not contain: ${needle}" >&2 - echo " stderr: ${last_stderr}" >&2 - exit 1 - fi -} - -test_validate_accepts_merge_and_release() { - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt - environments: - master: - apply_trigger: merge - production: - apply_trigger: release -YAML - bash "${script_dir}/validate-apply-trigger.sh" -} - -test_validate_rejects_missing_trigger() { - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt - environments: - master: - apply_trigger: merge - production: - aws_region: ap-northeast-1 -YAML - assert_fails bash "${script_dir}/validate-apply-trigger.sh" - assert_stderr_contains "environments.production.apply_trigger" -} - -test_validate_rejects_unknown_value() { - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt - environments: - production: - apply_trigger: relase -YAML - assert_fails bash "${script_dir}/validate-apply-trigger.sh" - assert_stderr_contains "(got 'relase')" -} - -test_validate_ignores_non_terragrunt_stacks() { - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt - environments: - master: - apply_trigger: merge - - name: kubernetes - paths: - - "kubernetes/components/{service}/{environment}" -YAML - bash "${script_dir}/validate-apply-trigger.sh" -} - -test_repository_config_is_valid() { - bash "${script_dir}/validate-apply-trigger.sh" "${repo_root}/workflow-config.yaml" -} - -test_repository_config_never_merges_production() { - local trigger - trigger="$(yq '.stacks[] | select(.name == "terragrunt") | .environments.production.apply_trigger' "${repo_root}/workflow-config.yaml")" - assert_eq "release" "${trigger}" -} - -test_release_service_parses_tag() { - assert_eq "eks-holmesgpt" "$(bash "${script_dir}/release-service.sh" --tag eks-holmesgpt-v1.2.3)" -} - -test_release_service_rejects_root_tag() { - assert_fails bash "${script_dir}/release-service.sh" --tag v0.1.0 - assert_stderr_contains "::error::" -} - -test_release_service_rejects_flux_tag() { - assert_fails bash "${script_dir}/release-service.sh" --tag kubernetes-production - assert_stderr_contains "::error::" -} - -test_release_service_parses_branch() { - assert_eq "vpc" "$(bash "${script_dir}/release-service.sh" --branch release-please--branches--main--components--vpc)" -} - -test_release_service_rejects_root_branch() { - assert_fails bash "${script_dir}/release-service.sh" --branch release-please--branches--main - assert_stderr_contains "::error::" -} - -test_release_service_error_is_single_line() { - assert_fails bash "${script_dir}/release-service.sh" --tag $'vpc-v1.2.3\n::warning::x' - assert_stderr_contains "::error::" - assert_eq "1" "$(printf '%s\n' "${last_stderr}" | wc -l | tr -d ' ')" -} - -write_release_config() { - local exclude="${1:-}" - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt-shared - attributes: {} - - name: terragrunt - environments: - master: - aws_region: ap-northeast-1 - iam_role_plan: arn:aws:iam::111111111111:role/master-plan - iam_role_apply: arn:aws:iam::111111111111:role/master-apply - apply_trigger: merge - production: - aws_region: ap-northeast-1 - iam_role_plan: arn:aws:iam::222222222222:role/production-plan - iam_role_apply: arn:aws:iam::222222222222:role/production-apply - apply_trigger: release - staging: - aws_region: us-east-1 - iam_role_plan: arn:aws:iam::333333333333:role/staging-plan - iam_role_apply: arn:aws:iam::333333333333:role/staging-apply - apply_trigger: release - - name: kubernetes - paths: - - "kubernetes/components/{service}/{environment}" -YAML - if [ -n "${exclude}" ]; then - EXCLUDE="${exclude}" yq -i '(.stacks[] | select(.name == "terragrunt") | .exclude) = env(EXCLUDE)' workflow-config.yaml - fi -} - -test_release_targets_lists_release_envs() { - write_release_config - mkdir -p aws/vpc/master aws/vpc/production - local expected='[{"service":"vpc","environment":"production","working_directory":"aws/vpc/production","aws_region":"ap-northeast-1","iam_role_plan":"arn:aws:iam::222222222222:role/production-plan","iam_role_apply":"arn:aws:iam::222222222222:role/production-apply"}]' - assert_eq "${expected}" "$(bash "${script_dir}/release-targets.sh" vpc)" -} - -test_release_targets_skips_missing_dirs() { - write_release_config - mkdir -p aws/vpc/production - local targets - targets="$(bash "${script_dir}/release-targets.sh" vpc)" - assert_eq "1" "$(jq 'length' <<<"${targets}")" - assert_eq "production" "$(jq -r '.[0].environment' <<<"${targets}")" -} - -test_release_targets_fails_without_targets() { - write_release_config - mkdir -p aws/route53/master - assert_fails bash "${script_dir}/release-targets.sh" route53 - assert_stderr_contains "no release targets for route53" -} - -test_release_targets_skips_excluded_envs() { - write_release_config '[{environment: production}]' - mkdir -p aws/vpc/production aws/vpc/staging - local targets - targets="$(bash "${script_dir}/release-targets.sh" vpc)" - assert_eq "1" "$(jq 'length' <<<"${targets}")" - assert_eq "staging" "$(jq -r '.[0].environment' <<<"${targets}")" -} - -test_release_targets_returns_empty_when_all_excluded() { - write_release_config '[{environment: production}, {environment: staging}]' - mkdir -p aws/vpc/production - assert_eq "[]" "$(bash "${script_dir}/release-targets.sh" vpc)" -} - -test_release_targets_honors_service_scoped_exclude() { - write_release_config '[{service: vpc, environment: production}]' - mkdir -p aws/vpc/production aws/eks/production - assert_eq "[]" "$(bash "${script_dir}/release-targets.sh" vpc)" - local targets - targets="$(bash "${script_dir}/release-targets.sh" eks)" - assert_eq "1" "$(jq 'length' <<<"${targets}")" - assert_eq "production" "$(jq -r '.[0].environment' <<<"${targets}")" -} - -test_release_targets_fails_without_dirs_even_with_exclude() { - write_release_config '[{environment: production}]' - mkdir -p aws/route53/master - assert_fails bash "${script_dir}/release-targets.sh" route53 - assert_stderr_contains "no release targets for route53" -} - -write_service_release_config() { - local service="$1" component="${2:-$1}" exclude_paths="${3:-[\"aws/$1/master\"]}" - mkdir -p "aws/${service}" - cat > "aws/${service}/release-please-config.json" < "aws/${service}/release-please-manifest.json" -} - -test_check_configs_accepts_matching_set() { - write_release_config - mkdir -p aws/vpc/production aws/route53/master - write_service_release_config vpc - assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" 2>&1)" -} - -test_check_configs_rejects_missing_config() { - write_release_config - mkdir -p aws/eks/production - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "aws/eks/release-please-config.json" -} - -test_check_configs_requires_config_for_excluded_env() { - write_release_config '[{environment: production}]' - mkdir -p aws/eks/production - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "aws/eks/release-please-config.json" - write_service_release_config eks - assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" 2>&1)" -} - -test_check_configs_rejects_orphan_config() { - write_release_config - mkdir -p aws/route53/master - write_service_release_config route53 - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "aws/route53/release-please-config.json" -} - -test_check_configs_rejects_wrong_component() { - write_release_config - mkdir -p aws/vpc/production - write_service_release_config vpc eks - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "aws/vpc: component must be vpc (got 'eks')" -} - -test_check_configs_rejects_manifest_without_package() { - write_release_config - mkdir -p aws/vpc/production - write_service_release_config vpc - echo '{}' > aws/vpc/release-please-manifest.json - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "aws/vpc/release-please-manifest.json" - rm aws/vpc/release-please-manifest.json - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "aws/vpc/release-please-manifest.json" -} - -test_check_configs_rejects_incomplete_release_env() { - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt - environments: - production: - apply_trigger: release -YAML - mkdir -p aws/vpc/production - write_service_release_config vpc - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "vpc/production" - assert_stderr_contains "aws_region" - assert_stderr_contains "iam_role_plan" - assert_stderr_contains "iam_role_apply" -} - -test_check_configs_rejects_missing_exclude_paths() { - write_release_config - mkdir -p aws/vpc/production - write_service_release_config vpc - jq -c 'del(.packages["aws/vpc"]["exclude-paths"])' aws/vpc/release-please-config.json > config.tmp - mv config.tmp aws/vpc/release-please-config.json - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains 'aws/vpc: exclude-paths must be ["aws/vpc/master"] (got null)' -} - -test_check_configs_rejects_relative_exclude_paths() { - write_release_config - mkdir -p aws/vpc/production - write_service_release_config vpc vpc '["master"]' - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains 'aws/vpc: exclude-paths must be ["aws/vpc/master"]' -} - -test_check_configs_accepts_exclude_paths_in_any_order() { - cat > workflow-config.yaml <<'YAML' -stacks: - - name: terragrunt - environments: - master: - apply_trigger: merge - develop: - apply_trigger: merge - production: - aws_region: ap-northeast-1 - iam_role_plan: arn:aws:iam::222222222222:role/production-plan - iam_role_apply: arn:aws:iam::222222222222:role/production-apply - apply_trigger: release -YAML - mkdir -p aws/vpc/production - write_service_release_config vpc vpc '["aws/vpc/develop","aws/vpc/master"]' - assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" 2>&1)" -} - -test_deploy_trigger_applies_only_merge_targets() { - local expected="\${{ matrix.target.stack == 'terragrunt' && (github.event_name == 'pull_request' && 'plan' || (matrix.target.apply_trigger == 'merge' && 'apply' || '')) || '' }}" - local actual - actual="$(yq '.jobs.deploy-terragrunt.with.action-type' "${repo_root}/.github/workflows/label-base-deploy--resolver.yaml")" - assert_eq "${expected}" "${actual}" -} - -test_deploy_trigger_excludes_shared_targets() { - local job stack - for job in deploy-terragrunt deploy-kubernetes; do - stack="$(yq ".jobs.${job}.strategy.matrix.exclude[] | select(.target.stack == \"terragrunt-shared\") | .target.stack" "${repo_root}/.github/workflows/label-base-deploy--resolver.yaml")" - assert_eq "terragrunt-shared" "${stack}" - done -} - -test_release_workflows_skip_empty_targets() { - local expected="needs.resolve.outputs.targets != '[]'" - assert_eq "${expected}" "$(yq '.jobs.plan.if' "${repo_root}/.github/workflows/release--plan.yaml")" - assert_eq "${expected}" "$(yq '.jobs.apply.if' "${repo_root}/.github/workflows/release--deploy.yaml")" -} - -test_repository_config_excludes_production() { - local excluded - excluded="$(yq '.stacks[] | select(.name == "terragrunt") | .exclude[] | select(.environment == "production") | .environment' "${repo_root}/workflow-config.yaml")" - assert_eq "production" "${excluded}" -} - -failed=0 -for test_name in $(compgen -A function test_); do - fixture="$(mktemp -d "${work_root}/fixture.XXXXXX")" - set +e - (set -e; cd "${fixture}"; "${test_name}") - status=$? - set -e - if [ "${status}" -eq 0 ]; then - echo "PASS: ${test_name}" - else - echo "FAIL: ${test_name}" - failed=1 - fi -done - -exit "${failed}" diff --git a/.github/scripts/terragrunt-release/validate-apply-trigger.sh b/.github/scripts/terragrunt-release/validate-apply-trigger.sh deleted file mode 100755 index dacd5a44..00000000 --- a/.github/scripts/terragrunt-release/validate-apply-trigger.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -config_path="${1:-workflow-config.yaml}" - -errors="$(yq -o=json '.' "${config_path}" | jq -r --arg q "'" ' - .stacks | to_entries[] - | select(.value.name == "terragrunt") - | .key as $index - | .value.environments | to_entries[] - | select(.value.apply_trigger | IN("merge", "release") | not) - | "::error::stacks[\($index)].environments.\(.key).apply_trigger must be merge or release (got \($q)\(.value.apply_trigger)\($q))" -')" - -if [ -n "${errors}" ]; then - echo "${errors}" >&2 - exit 1 -fi diff --git a/.github/workflows/ci-gatekeeper.yaml b/.github/workflows/ci-gatekeeper.yaml index a68a8daa..3295dff1 100644 --- a/.github/workflows/ci-gatekeeper.yaml +++ b/.github/workflows/ci-gatekeeper.yaml @@ -7,19 +7,11 @@ on: branches: - '**' -permissions: - actions: read - jobs: ci-gatekeeper: - name: CI Gatekeeper - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Wait for other workflows - uses: int128/wait-for-workflows-action@b320164b37276db89a12829914d6eb6ef4973d2b # v1.108.0 - with: - max-timeout-seconds: 1800 # 30 minutes - # The AI review is advisory; a Bedrock outage must not block merges through this required check. - exclude-workflow-names: | - Renovate Major Review + permissions: + actions: read + uses: panicboat/.github/.github/workflows/reusable--ci-gatekeeper.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 + with: + # The AI review is advisory; a Bedrock outage must not block merges through this required check. + exclude-workflow-names: Renovate Major Review diff --git a/.github/workflows/label-base-deploy--dispatcher.yaml b/.github/workflows/label-base-deploy--dispatcher.yaml index 6e9f35c2..14d75a72 100644 --- a/.github/workflows/label-base-deploy--dispatcher.yaml +++ b/.github/workflows/label-base-deploy--dispatcher.yaml @@ -7,37 +7,14 @@ on: branches: - '**' -permissions: - contents: read - pull-requests: write - issues: write - jobs: dispatch-labels: - name: 'Dispatch Labels' - if: ${{ !startsWith(github.head_ref, 'release-please--') }} - runs-on: ubuntu-latest - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Get PR information - id: pr-info - uses: jwalton/gh-find-current-pr@f3d61b485d2801773f7a07b2aaa3306bd8f8e653 # v1.3.5 - with: - github-token: ${{ steps.app-token.outputs.token }} - state: all - continue-on-error: true - - - name: Dispatch Labels - uses: panicboat/deploy-actions/label-dispatcher@1ab520e5ef33b8d744982762ffc4e08009a34744 # v2.0.1 - with: - pr-number: ${{ steps.pr-info.outputs.number }} - repository: ${{ github.repository }} - github-token: ${{ steps.app-token.outputs.token }} - config-path: 'workflow-config.yaml' + permissions: + contents: read + pull-requests: write + issues: write + uses: panicboat/.github/.github/workflows/reusable--label-base-deploy--dispatcher.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 + with: + app-id: ${{ vars.APP_ID }} + secrets: + private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/label-base-deploy--resolver.yaml b/.github/workflows/label-base-deploy--resolver.yaml index 3bd1aae0..0a6415ae 100644 --- a/.github/workflows/label-base-deploy--resolver.yaml +++ b/.github/workflows/label-base-deploy--resolver.yaml @@ -10,154 +10,18 @@ on: branches: - main -permissions: - id-token: write - contents: write # kubernetes-executor pushes hydrated manifests - pull-requests: write - concurrency: group: label-base-deploy-resolver-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - resolve: - name: 'Resolve Targets' - runs-on: ubuntu-latest - outputs: - targets: ${{ steps.resolver.outputs.targets }} - has-targets: ${{ steps.resolver.outputs.has-targets }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Validate apply_trigger - run: bash .github/scripts/terragrunt-release/validate-apply-trigger.sh - - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Get PR information - id: pr-info - uses: jwalton/gh-find-current-pr@f3d61b485d2801773f7a07b2aaa3306bd8f8e653 # v1.3.5 - with: - github-token: ${{ steps.app-token.outputs.token }} - state: all - continue-on-error: true - - - name: Label Resolver - id: resolver - uses: panicboat/deploy-actions/label-resolver@1ab520e5ef33b8d744982762ffc4e08009a34744 # v2.0.1 - with: - repository: ${{ github.repository }} - pr-number: ${{ steps.pr-info.outputs.number }} - github-token: ${{ steps.app-token.outputs.token }} - config-path: 'workflow-config.yaml' - - kubernetes-targets-group: - name: 'Group Kubernetes Targets by Environment' - needs: resolve - if: | - needs.resolve.outputs.has-targets == 'true' && - contains(needs.resolve.outputs.targets, '"stack":"kubernetes"') - runs-on: ubuntu-latest - outputs: - environments: ${{ steps.group.outputs.environments }} - steps: - - name: Group by environment - id: group - env: - TARGETS: ${{ needs.resolve.outputs.targets }} - run: | - set -euo pipefail - grouped=$(echo "$TARGETS" | jq -c ' - [.[] | select(.stack == "kubernetes")] - | group_by(.environment) - | map({environment: .[0].environment, services: [.[].service]}) - ') - echo "environments=$grouped" >> "$GITHUB_OUTPUT" - - deploy-kubernetes-hydrate: - name: 'Hydrate Kubernetes (${{ matrix.env.environment }})' - needs: kubernetes-targets-group - strategy: - matrix: - env: ${{ fromJson(needs.kubernetes-targets-group.outputs.environments) }} - fail-fast: false - uses: ./.github/workflows/reusable--kubernetes-hydrator.yaml - with: - environment: ${{ matrix.env.environment }} - services: ${{ toJson(matrix.env.services) }} - app-id: ${{ vars.APP_ID }} - secrets: - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - deploy-terragrunt: - name: 'Deploy Terragrunt (${{ matrix.target.service }}:${{ matrix.target.environment }})' - needs: resolve - if: | - needs.resolve.outputs.has-targets == 'true' && - contains(needs.resolve.outputs.targets, '"stack":"terragrunt"') - strategy: - matrix: - target: ${{ fromJson(needs.resolve.outputs.targets) }} - exclude: - - target: - stack: terragrunt-shared - fail-fast: false - uses: ./.github/workflows/reusable--terragrunt-runner.yaml + resolver: + permissions: + id-token: write + contents: write # the helmfile hydrator pushes hydrated manifests + pull-requests: write + uses: panicboat/.github/.github/workflows/reusable--label-base-deploy--resolver.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 with: - service-name: ${{ matrix.target.service }} - environment: ${{ matrix.target.environment }} - action-type: ${{ matrix.target.stack == 'terragrunt' && (github.event_name == 'pull_request' && 'plan' || (matrix.target.apply_trigger == 'merge' && 'apply' || '')) || '' }} - iam-role: ${{ github.event_name == 'pull_request' && matrix.target.iam_role_plan || matrix.target.iam_role_apply }} - aws-region: ${{ matrix.target.aws_region }} - working-directory: ${{ matrix.target.working_directory }} app-id: ${{ vars.APP_ID }} secrets: private-key: ${{ secrets.APP_PRIVATE_KEY }} - - deploy-kubernetes: - name: 'Deploy Kubernetes (${{ matrix.target.service }}:${{ matrix.target.environment }})' - needs: [resolve, deploy-kubernetes-hydrate] - if: | - needs.resolve.outputs.has-targets == 'true' && - contains(needs.resolve.outputs.targets, '"stack":"kubernetes"') - strategy: - matrix: - target: ${{ fromJson(needs.resolve.outputs.targets) }} - exclude: - - target: - stack: terragrunt - - target: - stack: terragrunt-shared - fail-fast: false - uses: ./.github/workflows/reusable--kubernetes-builder.yaml - with: - service-name: ${{ matrix.target.service }} - environment: ${{ matrix.target.environment }} - app-id: ${{ vars.APP_ID }} - secrets: - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - deployment-summary: - name: 'Deployment Summary' - needs: [resolve, deploy-terragrunt, deploy-kubernetes-hydrate, deploy-kubernetes] - if: always() && needs.resolve.outputs.has-targets == 'true' - runs-on: ubuntu-latest - steps: - - name: Summary - run: | - echo "📊 Deployment Summary" - echo "Event: ${{ github.event_name }}" - echo "Branch: ${{ github.ref_name }}" - echo "Environment: ${{ needs.resolve.outputs.target-environment }}" - echo "Safety Status: ${{ needs.resolve.outputs.safety-status }}" - echo "Targets: ${{ needs.resolve.outputs.targets }}" - echo "Terragrunt Job Status: ${{ needs.deploy-terragrunt.result }}" - echo "Kubernetes Job Status: ${{ needs.deploy-kubernetes.result }}" - echo "Kubernetes Hydrate Job Status: ${{ needs.deploy-kubernetes-hydrate.result }}" diff --git a/.github/workflows/lint-actions.yml b/.github/workflows/lint-actions.yml index 9d7bb5ad..a05997e6 100644 --- a/.github/workflows/lint-actions.yml +++ b/.github/workflows/lint-actions.yml @@ -6,13 +6,8 @@ on: branches: [main] pull_request: -permissions: - contents: read - jobs: ensure-sha-pinned: - name: Ensure actions are pinned to SHAs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@c4e71056006d29f90204b2d063be337109507d62 # v5.0.10 + permissions: + contents: read + uses: panicboat/.github/.github/workflows/reusable--lint-actions.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 diff --git a/.github/workflows/release--deploy.yaml b/.github/workflows/release--deploy.yaml index eb1463b2..c5283509 100644 --- a/.github/workflows/release--deploy.yaml +++ b/.github/workflows/release--deploy.yaml @@ -11,62 +11,18 @@ on: required: true type: string -permissions: - id-token: write - contents: read - pull-requests: write - concurrency: group: release-deploy-${{ github.event.release.tag_name || inputs.tag }} cancel-in-progress: false jobs: - resolve: - name: 'Resolve Release Targets' - runs-on: ubuntu-latest + release-deploy: permissions: + id-token: write contents: read - env: - TAG: ${{ github.event.release.tag_name || inputs.tag }} - outputs: - targets: ${{ steps.resolve.outputs.targets }} - tag: ${{ steps.resolve.outputs.tag }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ env.TAG }} - - - name: Resolve release targets - id: resolve - run: | - set -euo pipefail - service=$(bash .github/scripts/terragrunt-release/release-service.sh --tag "$TAG") - targets=$(bash .github/scripts/terragrunt-release/release-targets.sh "$service") - echo "targets=$targets" >> "$GITHUB_OUTPUT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - apply: - name: 'Apply Release (${{ matrix.target.service }}:${{ matrix.target.environment }})' - needs: resolve - if: needs.resolve.outputs.targets != '[]' - concurrency: - group: terragrunt-apply-${{ matrix.target.working_directory }} - cancel-in-progress: false - strategy: - matrix: - target: ${{ fromJson(needs.resolve.outputs.targets) }} - fail-fast: false - uses: ./.github/workflows/reusable--terragrunt-runner.yaml + pull-requests: write + uses: panicboat/.github/.github/workflows/reusable--release--deploy.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 with: - service-name: ${{ matrix.target.service }} - environment: ${{ matrix.target.environment }} - action-type: apply - iam-role: ${{ matrix.target.iam_role_apply }} - aws-region: ${{ matrix.target.aws_region }} - working-directory: ${{ matrix.target.working_directory }} app-id: ${{ vars.APP_ID }} - ref: ${{ needs.resolve.outputs.tag }} - github-environment: ${{ matrix.target.environment }} secrets: private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/release--plan.yaml b/.github/workflows/release--plan.yaml index d2caabaa..223486c5 100644 --- a/.github/workflows/release--plan.yaml +++ b/.github/workflows/release--plan.yaml @@ -5,54 +5,18 @@ on: pull_request: types: [opened, synchronize, reopened] -permissions: - id-token: write - contents: read - pull-requests: write - concurrency: group: release-plan-${{ github.head_ref }} cancel-in-progress: true jobs: - resolve: - name: 'Resolve Release Targets' - if: startsWith(github.head_ref, 'release-please--branches--main--components--') - runs-on: ubuntu-latest + release-plan: permissions: + id-token: write contents: read - outputs: - targets: ${{ steps.resolve.outputs.targets }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Resolve release targets - id: resolve - env: - HEAD_REF: ${{ github.head_ref }} - run: | - set -euo pipefail - service=$(bash .github/scripts/terragrunt-release/release-service.sh --branch "$HEAD_REF") - targets=$(bash .github/scripts/terragrunt-release/release-targets.sh "$service") - echo "targets=$targets" >> "$GITHUB_OUTPUT" - - plan: - name: 'Plan Release (${{ matrix.target.service }}:${{ matrix.target.environment }})' - needs: resolve - if: needs.resolve.outputs.targets != '[]' - strategy: - matrix: - target: ${{ fromJson(needs.resolve.outputs.targets) }} - fail-fast: false - uses: ./.github/workflows/reusable--terragrunt-runner.yaml + pull-requests: write + uses: panicboat/.github/.github/workflows/reusable--release--plan.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 with: - service-name: ${{ matrix.target.service }} - environment: ${{ matrix.target.environment }} - action-type: plan - iam-role: ${{ matrix.target.iam_role_plan }} - aws-region: ${{ matrix.target.aws_region }} - working-directory: ${{ matrix.target.working_directory }} app-id: ${{ vars.APP_ID }} secrets: private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/release--please.yaml b/.github/workflows/release--please.yaml index abd973bb..5f0e5301 100644 --- a/.github/workflows/release--please.yaml +++ b/.github/workflows/release--please.yaml @@ -6,46 +6,16 @@ on: branches: [main] workflow_dispatch: {} -permissions: - contents: read - concurrency: group: release-please cancel-in-progress: false jobs: - discover: - runs-on: ubuntu-latest - outputs: - paths: ${{ steps.find.outputs.paths }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Find release-please configs - id: find - run: | - set -euo pipefail - paths=$(git ls-files '*/release-please-config.json' | jq -R 'rtrimstr("/release-please-config.json")' | jq -sc .) - echo "paths=$paths" >> "$GITHUB_OUTPUT" - release-please: - needs: discover - if: needs.discover.outputs.paths != '[]' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - path: ${{ fromJson(needs.discover.outputs.paths) }} - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5 - with: - token: ${{ steps.app-token.outputs.token }} - config-file: ${{ matrix.path }}/release-please-config.json - manifest-file: ${{ matrix.path }}/release-please-manifest.json + permissions: + contents: read + uses: panicboat/.github/.github/workflows/reusable--release--please.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 + with: + app-id: ${{ vars.APP_ID }} + secrets: + private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/renovate-major-review.yaml b/.github/workflows/renovate-major-review.yaml index a1d24a84..557e7e40 100644 --- a/.github/workflows/renovate-major-review.yaml +++ b/.github/workflows/renovate-major-review.yaml @@ -13,105 +13,11 @@ concurrency: cancel-in-progress: true jobs: - render: - name: 'Render' - if: github.event.label.name == '📈 major-update' && github.event.pull_request.user.login == 'renovate[bot]' - runs-on: ubuntu-latest - timeout-minutes: 15 - # helmfile templates can exec commands, so PR content is rendered in a job without id-token or the review environment. - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Setup aqua - uses: aquaproj/aqua-installer@82d0e83878a8657c4a6f2ff1755d3bfbfc67f329 # v4.1.0 - with: - aqua_version: v2.64.0 - - - name: Render manifest diff - # HEAD is the PR merge commit, so its first parent is the base it merges into. - run: bash .github/scripts/kubernetes-hydrate/render-diff.sh HEAD^1 "${RUNNER_TEMP}/render-diff" - - - name: Upload manifest diff - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: render-diff - path: ${{ runner.temp }}/render-diff - retention-days: 1 - review: - name: 'Review' - needs: render - runs-on: ubuntu-latest - timeout-minutes: 30 - # Environment-scoped OIDC sub keeps a prompt-injected run from assuming the plan roles, which trust the pull_request sub. - environment: renovate-major-review permissions: contents: read - pull-requests: read - id-token: write - outputs: - body: ${{ steps.claude.outputs.structured_output && fromJSON(steps.claude.outputs.structured_output).body || '' }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Download manifest diff - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - name: render-diff - path: .render-diff - - - name: Setup aqua - uses: aquaproj/aqua-installer@82d0e83878a8657c4a6f2ff1755d3bfbfc67f329 # v4.1.0 - with: - aqua_version: v2.64.0 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - with: - role-to-assume: arn:aws:iam::559744160976:role/github-oidc-auth-master-github-actions-bedrock-role - aws-region: us-east-1 - - - name: Review with Claude - id: claude - uses: anthropics/claude-code-action@16f9f71ab9c490faf367f94dd294897edc25be33 # v1.0.261 - env: - # The Bedrock role only allows Sonnet 4.6, so background tasks must not fall back to Haiku. - ANTHROPIC_DEFAULT_HAIKU_MODEL: global.anthropic.claude-sonnet-4-6 - with: - use_bedrock: 'true' - # Read-only token; without it the action exchanges OIDC for the Claude App's write token. - github_token: ${{ github.token }} - allowed_bots: renovate - prompt: | - Read `.github/prompts/renovate-major-review.md` and follow it. - Pull request number: ${{ github.event.pull_request.number }} - claude_args: | - --model global.anthropic.claude-sonnet-4-6 - --json-schema '{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":60000}},"required":["body"]}' - --allowedTools "Read,Glob,Grep,WebFetch,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh api:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(helm repo add:*),Bash(helm repo update:*),Bash(helm pull:*),Bash(helm show:*)" - - post: - name: 'Post Review' - needs: review - if: needs.review.result == 'success' && needs.review.outputs.body != '' - runs-on: ubuntu-latest - permissions: pull-requests: write - steps: - - name: Upsert review comment - uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 - with: - message: ${{ needs.review.outputs.body }} - comment-tag: renovate-major-review - mode: upsert - pr-number: ${{ github.event.pull_request.number }} + id-token: write + uses: panicboat/.github/.github/workflows/reusable--renovate-major-review.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 + with: + prompt-path: .github/prompts/renovate-major-review.md diff --git a/.github/workflows/reusable--kubernetes-builder.yaml b/.github/workflows/reusable--kubernetes-builder.yaml deleted file mode 100644 index 816e6168..00000000 --- a/.github/workflows/reusable--kubernetes-builder.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# Posts the kustomize diff of one hydrated component as a pull request comment. -name: 'Reusable - Kubernetes Builder' - -on: - workflow_call: - inputs: - service-name: - required: true - type: string - description: 'Service name for identification' - environment: - required: true - type: string - description: 'Target environment (e.g., local)' - app-id: - required: true - type: string - description: 'GitHub App ID for authentication' - secrets: - private-key: - required: true - description: 'GitHub App private key for authentication' - -jobs: - kustomize-diff: - name: 'Kustomize Diff' - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ inputs.app-id }} - private-key: ${{ secrets.private-key }} - owner: ${{ github.repository_owner }} - - - name: Get PR information - id: pr-info - uses: jwalton/gh-find-current-pr@f3d61b485d2801773f7a07b2aaa3306bd8f8e653 # v1.3.5 - with: - github-token: ${{ steps.app-token.outputs.token }} - state: all - continue-on-error: true - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - token: ${{ steps.app-token.outputs.token }} - ref: ${{ github.head_ref }} - fetch-depth: 0 - - - name: Checkout base ref - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event.pull_request.base.sha }} - path: base - token: ${{ steps.app-token.outputs.token }} - - - name: Build head manifests - id: kustomize-head - uses: int128/kustomize-action@4fbeca93d7072507f0c6d4529025d17a5d209551 # v1.195.0 - with: - kustomization: kubernetes/manifests/${{ inputs.environment }}/${{ inputs.service-name }}/kustomization.yaml - write-individual-files: true - - - name: Build base manifests - id: kustomize-base - uses: int128/kustomize-action@4fbeca93d7072507f0c6d4529025d17a5d209551 # v1.195.0 - with: - base-directory: base - kustomization: kubernetes/manifests/${{ inputs.environment }}/${{ inputs.service-name }}/kustomization.yaml - write-individual-files: true - - - name: Diff manifests - id: diff - uses: int128/diff-action@274034d712aef611adcbd41e5a647f43e4edd5a5 # v2.41.0 - with: - base: ${{ steps.kustomize-base.outputs.directory }} - head: ${{ steps.kustomize-head.outputs.directory }} - - - name: Comment PR - if: steps.pr-info.outputs.number != '' - uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 - with: - message: | - ## Kustomize Diff - - **Service**: `${{ inputs.service-name }}` - **Environment**: `${{ inputs.environment }}` - - ${{ steps.diff.outputs.comment-body || 'No changes' }} - comment-tag: "kubernetes-${{ inputs.service-name }}-${{ inputs.environment }}" - mode: upsert - pr-number: ${{ steps.pr-info.outputs.number }} - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - reactions: ${{ steps.diff.outputs.has-diff == 'true' && 'rocket' || '' }} - continue-on-error: true diff --git a/.github/workflows/reusable--kubernetes-hydrator.yaml b/.github/workflows/reusable--kubernetes-hydrator.yaml deleted file mode 100644 index 3f6d2b70..00000000 --- a/.github/workflows/reusable--kubernetes-hydrator.yaml +++ /dev/null @@ -1,125 +0,0 @@ -# Renders the given components into kubernetes/manifests and commits the result to the pull request branch. -name: 'Reusable - Kubernetes Hydrator' - -on: - workflow_call: - inputs: - environment: - required: true - type: string - description: 'Target environment for hydration (e.g., local)' - services: - required: true - type: string - description: 'JSON array of service names to hydrate (e.g. ["coredns","cilium"])' - app-id: - required: true - type: string - description: 'GitHub App ID for authentication' - secrets: - private-key: - required: true - description: 'GitHub App private key for authentication' - -concurrency: - group: kubernetes-hydrate-${{ github.event.pull_request.number || github.ref }}-${{ inputs.environment }} - cancel-in-progress: false - -jobs: - hydrate: - name: 'Hydrate' - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ inputs.app-id }} - private-key: ${{ secrets.private-key }} - owner: ${{ github.repository_owner }} - - - name: Get PR information - id: pr-info - uses: jwalton/gh-find-current-pr@f3d61b485d2801773f7a07b2aaa3306bd8f8e653 # v1.3.5 - with: - github-token: ${{ steps.app-token.outputs.token }} - state: all - continue-on-error: true - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - token: ${{ steps.app-token.outputs.token }} - ref: ${{ github.head_ref }} - fetch-depth: 0 - - - name: Setup aqua - uses: aquaproj/aqua-installer@82d0e83878a8657c4a6f2ff1755d3bfbfc67f329 # v4.1.0 - with: - aqua_version: v2.64.0 - - - name: Hydrate changed components - env: - SERVICES_JSON: ${{ inputs.services }} - run: | - set -euo pipefail - for svc in $(echo "$SERVICES_JSON" | jq -r '.[]'); do - bash .github/scripts/kubernetes-hydrate/hydrate-component.sh "$svc" "${{ inputs.environment }}" - done - - - name: Hydrate index - run: bash .github/scripts/kubernetes-hydrate/hydrate-index.sh "${{ inputs.environment }}" - - - name: Commit and push hydrated manifests - id: auto-commit - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 - with: - commit_message: 'chore(kubernetes/manifests/${{ inputs.environment }}): hydrate manifests' - commit_options: '--signoff' - file_pattern: 'kubernetes/manifests/${{ inputs.environment }}/' - commit_user_name: 'github-actions[bot]' - commit_user_email: 'github-actions[bot]@users.noreply.github.com' - - - name: Compute index diff - id: index-diff - if: steps.pr-info.outputs.number != '' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - ENV: ${{ inputs.environment }} - run: | - set -euo pipefail - diff=$(git diff "$BASE_SHA" HEAD -- \ - "kubernetes/manifests/$ENV/kustomization.yaml" \ - "kubernetes/manifests/$ENV/00-namespaces/" || true) - { - echo 'diff<> "$GITHUB_OUTPUT" - if [ -n "$diff" ]; then - echo "has-diff=true" >> "$GITHUB_OUTPUT" - else - echo "has-diff=false" >> "$GITHUB_OUTPUT" - fi - - - name: Comment index diff - if: steps.index-diff.outputs.has-diff == 'true' && steps.pr-info.outputs.number != '' - uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 - with: - message: | - ## Kubernetes Index Diff - - **Environment**: `${{ inputs.environment }}` - - ```diff - ${{ steps.index-diff.outputs.diff }} - ``` - comment-tag: 'kubernetes-index-${{ inputs.environment }}' - mode: upsert - pr-number: ${{ steps.pr-info.outputs.number }} - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - continue-on-error: true diff --git a/.github/workflows/reusable--terragrunt-runner.yaml b/.github/workflows/reusable--terragrunt-runner.yaml deleted file mode 100644 index 35342ff3..00000000 --- a/.github/workflows/reusable--terragrunt-runner.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# Runs terragrunt plan or apply in one working directory and reports the result with tfcmt. -name: 'Reusable - Terragrunt Runner' - -on: - workflow_call: - inputs: - service-name: - required: true - type: string - description: 'Service name for terragrunt execution' - environment: - required: true - type: string - description: 'Environment name (develop, staging, production, etc.)' - action-type: - required: true - type: string - description: 'Action type: plan or apply' - iam-role: - required: true - type: string - description: 'IAM role for plan/apply operations' - aws-region: - required: true - type: string - description: 'AWS region' - working-directory: - required: true - type: string - description: 'Working directory for terragrunt execution' - app-id: - required: true - type: string - description: 'GitHub App ID for authentication' - ref: - required: false - type: string - default: '' - description: 'Git ref to check out (defaults to the triggering ref)' - github-environment: - required: false - type: string - default: '' - description: 'GitHub environment the job runs in (none when empty)' - secrets: - private-key: - required: true - description: 'GitHub App private key for authentication' - -jobs: - run-terragrunt: - name: 'Run Terragrunt' - if: inputs.action-type != '' - runs-on: ubuntu-latest - environment: ${{ inputs.github-environment }} - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ inputs.app-id }} - private-key: ${{ secrets.private-key }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.ref }} - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - with: - role-to-assume: ${{ inputs.iam-role }} - aws-region: ${{ inputs.aws-region }} - role-session-name: GitHubActions-Terragrunt-${{ inputs.action-type }}-${{ inputs.environment }} - audience: sts.amazonaws.com - - - name: Setup aqua - uses: aquaproj/aqua-installer@82d0e83878a8657c4a6f2ff1755d3bfbfc67f329 # v4.1.0 - with: - aqua_version: v2.64.0 - - - name: Run Terragrunt - working-directory: ${{ inputs.working-directory }} - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - TF_INPUT: 'false' - # tfcmt cannot parse the output while terragrunt prefixes each line with its log metadata. - TG_LOG_DISABLE: 'true' - TFCMT_PLAN_PATCH: 'true' - TFCMT_DISABLE_LABEL: 'true' - SERVICE_NAME: ${{ inputs.service-name }} - ENVIRONMENT: ${{ inputs.environment }} - ACTION_TYPE: ${{ inputs.action-type }} - run: tfcmt -var "target:$SERVICE_NAME/$ENVIRONMENT" "$ACTION_TYPE" -- terragrunt "$ACTION_TYPE" -no-color ${{ inputs.action-type == 'apply' && '-auto-approve' || '' }} diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index a9393ed5..a2e5238e 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -5,21 +5,12 @@ on: pull_request_target: types: [opened, edited, synchronize, reopened] -permissions: - contents: read - jobs: validate: - name: Validate PR title - runs-on: ubuntu-latest - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + permissions: + contents: read + uses: panicboat/.github/.github/workflows/reusable--semantic-pull-request.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 + with: + app-id: ${{ vars.APP_ID }} + secrets: + private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/test-terragrunt-release.yaml b/.github/workflows/test-terragrunt-release.yaml index 414f8aea..204128b5 100644 --- a/.github/workflows/test-terragrunt-release.yaml +++ b/.github/workflows/test-terragrunt-release.yaml @@ -1,4 +1,4 @@ -# Tests the terragrunt release scripts and validates workflow-config.yaml and the release-please configs. +# Validates workflow-config.yaml apply_trigger values and the release-please configs. name: Test Terragrunt Release on: @@ -6,15 +6,8 @@ on: branches: [main] pull_request: -permissions: - contents: read - jobs: - test: - name: Test terragrunt release scripts - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - run: bash .github/scripts/terragrunt-release/test.sh - - run: bash .github/scripts/terragrunt-release/validate-apply-trigger.sh - - run: bash .github/scripts/terragrunt-release/check-release-configs.sh + validate: + permissions: + contents: read + uses: panicboat/.github/.github/workflows/reusable--test-terragrunt-release.yaml@d54532665675f1843914da4b324dc2a58dd37239 # v1.0.0 diff --git a/README-ja.md b/README-ja.md index 9bcf0d7a..08db75a6 100644 --- a/README-ja.md +++ b/README-ja.md @@ -53,7 +53,7 @@ #### Releases - `apply_trigger: release` の environment を持つ `aws/{service}` は、それぞれ release-please の設定(`release-please-config.json` / `release-please-manifest.json`)を持つ。 - - 設定がないと [check-release-configs.sh](.github/scripts/terragrunt-release/check-release-configs.sh) が CI を失敗させる。 + - 設定がないと [check-release-configs.sh](https://github.com/panicboat/.github/blob/main/scripts/terragrunt-release/check-release-configs.sh) が CI を失敗させる。 - `apply_trigger: merge` の environment ディレクトリだけを変える commit は、release を作らない(`exclude-paths`)。 - `apply_trigger: release` の environment がすべて除外されている間も release PR は作られるが、plan も apply もされない。 - plan を出し直すときは、`Release - Plan` の run で「Re-run all jobs」を使う。 @@ -63,9 +63,9 @@ | Stack | Path convention | Tooling | |-------|-----------------|---------| -| AWS infrastructure | `aws/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](.github/workflows/reusable--terragrunt-runner.yaml)) | -| GitHub configuration | `github/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](.github/workflows/reusable--terragrunt-runner.yaml)) | -| Kubernetes platform | `kubernetes/components/{service}/{environment}` | Helmfile → Kustomize hydration ([reusable--kubernetes-hydrator.yaml](.github/workflows/reusable--kubernetes-hydrator.yaml) / [reusable--kubernetes-builder.yaml](.github/workflows/reusable--kubernetes-builder.yaml)) → Flux CD | +| AWS infrastructure | `aws/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--terragrunt-runner.yaml)) | +| GitHub configuration | `github/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--terragrunt-runner.yaml)) | +| Kubernetes platform | `kubernetes/components/{service}/{environment}` | Helmfile → Kustomize hydration ([reusable--kubernetes-hydrator.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--kubernetes-hydrator.yaml) / [reusable--kubernetes-builder.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--kubernetes-builder.yaml)) → Flux CD | ### Environments and Authentication @@ -92,7 +92,7 @@ flowchart LR end subgraph KubernetesCI [Kubernetes CI] - Group[kubernetes-targets-group
group by env] + Group[helmfile-targets-group
group by env] Hydrator[reusable--kubernetes-hydrator
matrix: env
concurrency: hydrate-PR-env] Commit[auto-commit
kubernetes/manifests/] Builder[reusable--kubernetes-builder
matrix: service x env
diff only] @@ -124,7 +124,7 @@ flowchart LR TG -->|on push main or release published| Apply Plan --> PRComment Apply --> AWS - Resolver -->|stack: kubernetes| Group + Resolver -->|stack: helmfile| Group Group --> Hydrator Hydrator -->|hydrate-component.sh
+ hydrate-index.sh| Commit Hydrator -->|index diff| IndexComment @@ -139,4 +139,4 @@ flowchart LR - Flux bootstrap は `kubernetes/clusters/{cluster}/flux-system/` に置く。cluster の root Kustomization が `kubernetes/manifests/{cluster}/` 配下のレンダリング済みマニフェストと外部 `GitRepository`(現状はアプリケーション monorepo)を集約する。 - Platform と application の repository は別々に pull され、疎結合に保たれる。同期間隔や source URL は `kubernetes/clusters/{cluster}/` 配下のマニフェストが source of truth。 -- [kubernetes/components/](kubernetes/components/) を変更する PR では CI が [.github/scripts/kubernetes-hydrate/](.github/scripts/kubernetes-hydrate/) を実行し、レンダリング結果を [kubernetes/manifests/](kubernetes/manifests/) にコミット、差分を PR コメントに投稿する。マージ後に Flux が適用する。 +- [kubernetes/components/](kubernetes/components/) を変更する PR では CI が [scripts/kubernetes-hydrate/](https://github.com/panicboat/.github/tree/main/scripts/kubernetes-hydrate) を実行し、レンダリング結果を [kubernetes/manifests/](kubernetes/manifests/) にコミット、差分を PR コメントに投稿する。マージ後に Flux が適用する。 diff --git a/README.md b/README.md index 8f159a8c..9988063e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ A PR labeled by `panicboat/deploy-actions` is resolved against [workflow-config. #### Releases - Each `aws/{service}` with an `apply_trigger: release` environment has its own release-please config (`release-please-config.json` / `release-please-manifest.json`). - - [check-release-configs.sh](.github/scripts/terragrunt-release/check-release-configs.sh) fails CI when a config is missing. + - [check-release-configs.sh](https://github.com/panicboat/.github/blob/main/scripts/terragrunt-release/check-release-configs.sh) fails CI when a config is missing. - Commits that touch only `apply_trigger: merge` environment directories do not create a release (`exclude-paths`). - While every `apply_trigger: release` environment is excluded, the release PR is still created, but nothing is planned or applied. - To re-plan, use "Re-run all jobs" on the `Release - Plan` run. @@ -63,9 +63,9 @@ A PR labeled by `panicboat/deploy-actions` is resolved against [workflow-config. | Stack | Path convention | Tooling | |-------|-----------------|---------| -| AWS infrastructure | `aws/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](.github/workflows/reusable--terragrunt-runner.yaml)) | -| GitHub configuration | `github/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](.github/workflows/reusable--terragrunt-runner.yaml)) | -| Kubernetes platform | `kubernetes/components/{service}/{environment}` | Helmfile → Kustomize hydration ([reusable--kubernetes-hydrator.yaml](.github/workflows/reusable--kubernetes-hydrator.yaml) / [reusable--kubernetes-builder.yaml](.github/workflows/reusable--kubernetes-builder.yaml)) → Flux CD | +| AWS infrastructure | `aws/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--terragrunt-runner.yaml)) | +| GitHub configuration | `github/{service}/{environment}` | Terragrunt + OpenTofu ([reusable--terragrunt-runner.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--terragrunt-runner.yaml)) | +| Kubernetes platform | `kubernetes/components/{service}/{environment}` | Helmfile → Kustomize hydration ([reusable--kubernetes-hydrator.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--kubernetes-hydrator.yaml) / [reusable--kubernetes-builder.yaml](https://github.com/panicboat/.github/blob/main/.github/workflows/reusable--kubernetes-builder.yaml)) → Flux CD | ### Environments and authentication @@ -92,7 +92,7 @@ flowchart LR end subgraph KubernetesCI [Kubernetes CI] - Group[kubernetes-targets-group
group by env] + Group[helmfile-targets-group
group by env] Hydrator[reusable--kubernetes-hydrator
matrix: env
concurrency: hydrate-PR-env] Commit[auto-commit
kubernetes/manifests/] Builder[reusable--kubernetes-builder
matrix: service x env
diff only] @@ -124,7 +124,7 @@ flowchart LR TG -->|on push main or release published| Apply Plan --> PRComment Apply --> AWS - Resolver -->|stack: kubernetes| Group + Resolver -->|stack: helmfile| Group Group --> Hydrator Hydrator -->|hydrate-component.sh
+ hydrate-index.sh| Commit Hydrator -->|index diff| IndexComment @@ -139,4 +139,4 @@ flowchart LR - Flux bootstrap lives in `kubernetes/clusters/{cluster}/flux-system/`. The cluster's root Kustomization aggregates the rendered manifests under `kubernetes/manifests/{cluster}/` and any external `GitRepository` (currently the application monorepo). - Platform and application repositories are pulled independently and stay loosely coupled. Sync intervals and source URLs are configured in `kubernetes/clusters/{cluster}/` — treat those manifests as the source of truth. -- When [kubernetes/components/](kubernetes/components/) changes, CI runs [.github/scripts/kubernetes-hydrate/](.github/scripts/kubernetes-hydrate/), commits the rendered output to [kubernetes/manifests/](kubernetes/manifests/), and posts the diff as a PR comment. Flux applies the change once merged. +- When [kubernetes/components/](kubernetes/components/) changes, CI runs [scripts/kubernetes-hydrate/](https://github.com/panicboat/.github/tree/main/scripts/kubernetes-hydrate), commits the rendered output to [kubernetes/manifests/](kubernetes/manifests/), and posts the diff as a PR comment. Flux applies the change once merged. diff --git a/docs/runbooks/eks-production-recreate.md b/docs/runbooks/eks-production-recreate.md index 19c2e7b3..b9ebf27e 100644 --- a/docs/runbooks/eks-production-recreate.md +++ b/docs/runbooks/eks-production-recreate.md @@ -247,14 +247,14 @@ Karpenter が NodePool 観測 → Pending pod (= ebs-csi-controller / hubble-rel ```bash # RECREATE marker は Phase 4 で edit 済 (= 再 edit 不要) -# hydrate-component で全 component を新 cluster ID で再生成 +# hydrate-component で全 component を新 cluster ID で再生成 (scripts は platform の隣に clone した panicboat/.github にある) for comp in $(find kubernetes/components -mindepth 1 -maxdepth 1 -type d -exec basename {} \;); do if [ -d "kubernetes/components/${comp}/production" ]; then echo "Hydrating ${comp}..." - bash .github/scripts/kubernetes-hydrate/hydrate-component.sh "$comp" production + bash ../.github/scripts/kubernetes-hydrate/hydrate-component.sh "$comp" production fi done -bash .github/scripts/kubernetes-hydrate/hydrate-index.sh production +bash ../.github/scripts/kubernetes-hydrate/hydrate-index.sh production # commit + push (= main 直接 push は branch protection で blocked のため PR 経由) git checkout -b chore/hydrate-after-recreate-$(date +%Y%m%d) diff --git a/workflow-config.yaml b/workflow-config.yaml index 91f5b917..35f0e3e9 100644 --- a/workflow-config.yaml +++ b/workflow-config.yaml @@ -22,6 +22,6 @@ stacks: exclude: - environment: production - - name: kubernetes + - name: helmfile paths: - "kubernetes/components/{service}/{environment}"