Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/cdn-upload.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ jobs:
steps:
- name: Download build artifact
if: inputs.artifact_name != ''
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact_name }}
path: ${{ inputs.dist_dir }}

- name: Upload assets to Azure Blob Storage
uses: azure/CLI@v2
uses: azure/cli@v3
env:
SAS_TOKEN: ${{ secrets.CDN_SAS_TOKEN }}
CDN_ACCOUNT_NAME: ${{ inputs.cdn_account_name }}
Expand Down
121 changes: 121 additions & 0 deletions .github/workflows/helm-lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Reusable workflow: lint and (optionally) kubeconform-validate Helm charts.
#
# Language-agnostic — works for single-chart repos (chart_glob: "charts/")
# and monorepos with several charts (chart_glob: "charts/*/").
#
# Single-chart caller (sugar-suite / course-workload-estimator):
#
# jobs:
# helm-lint:
# uses: bcit-tlu/.github/.github/workflows/helm-lint.yaml@main
# with:
# chart_glob: "charts/"
# extra_command: bash tests/helm-cdn-rewrite.sh # optional
#
# Monorepo caller (hriv):
#
# jobs:
# helm-lint:
# uses: bcit-tlu/.github/.github/workflows/helm-lint.yaml@main
# with:
# chart_glob: "charts/*/"
# validate_continue_on_error: true
# extra_command: bash scripts/test-helm-chart-regressions.sh

name: Helm lint

on:
workflow_call:
inputs:
chart_glob:
description: "Space/glob-expandable list of chart directories to lint (e.g. 'charts/' or 'charts/*/')."
required: false
type: string
default: "charts/"
kubeconform_version:
description: "kubeconform release to install."
required: false
type: string
default: v0.6.7
validate_continue_on_error:
description: "Treat kubeconform validation failures as non-fatal (schema downloads can be flaky). Strict `helm lint` stays the hard gate."
required: false
type: boolean
default: false
extra_command:
description: "Optional shell command run after validation (e.g. a repo-specific chart test script)."
required: false
type: string
default: ""

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# Bring the chart sources into the runner.
- uses: actions/checkout@v7

# `helm` is used both to lint and to render charts for kubeconform.
- uses: azure/setup-helm@v5

# kubeconform renders + schema-validates the templated manifests.
- name: Install kubeconform
env:
KUBECONFORM_VERSION: ${{ inputs.kubeconform_version }}
run: |
curl -sSL "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" \
| sudo tar xz -C /usr/local/bin kubeconform

# Cache the downloaded JSON schemas so repeated runs don't re-fetch them
# (keyed on the kubeconform version + Chart.yaml set).
- name: Cache kubeconform schemas
uses: actions/cache@v5
with:
path: /tmp/kubeconform-cache
key: kubeconform-schemas-${{ inputs.kubeconform_version }}-${{ hashFiles('charts/**/Chart.yaml') }}
restore-keys: |
kubeconform-schemas-${{ inputs.kubeconform_version }}-

# Hard gate: `helm lint` each chart in the glob. Any failure fails CI.
- name: Lint charts (strict gate)
env:
CHART_GLOB: ${{ inputs.chart_glob }}
run: |
set -euo pipefail
for chart in ${CHART_GLOB}; do
echo "::group::helm lint ${chart}"
helm lint "${chart}"
echo "::endgroup::"
done

# kubeconform schema downloads from raw.githubusercontent.com can be flaky
# on shared runners. Callers that want the strict `helm lint` above to be
# the primary gate can set validate_continue_on_error to absorb transient
# download failures here.
- name: Validate rendered charts with kubeconform
continue-on-error: ${{ inputs.validate_continue_on_error }}
env:
CHART_GLOB: ${{ inputs.chart_glob }}
run: |
set -euo pipefail
mkdir -p /tmp/kubeconform-cache
for chart in ${CHART_GLOB}; do
echo "::group::kubeconform ${chart}"
helm template test "${chart}" \
| kubeconform -cache /tmp/kubeconform-cache -strict -summary \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
-ignore-missing-schemas
echo "::endgroup::"
done

# Optional repo-specific check (e.g. a chart regression/CDN-rewrite
# script) run only when the caller supplies `extra_command`.
- name: Run extra command
if: inputs.extra_command != ''
run: ${{ inputs.extra_command }}
220 changes: 220 additions & 0 deletions .github/workflows/helm-publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Reusable workflow: package, push, and cosign-sign a Helm chart to an OCI
# registry.
#
# Two version modes:
# * RC publish (from ci.yaml): pass `version` directly (e.g. the
# oci-build rc_version). Empty `version` = nothing to publish → skip.
# * Release publish (from a `release: published` workflow): pass
# `tag_name` + `tag_prefix`; the tag is parsed to a semver `version`.
# A tag that doesn't match the prefix is skipped (useful for monorepo
# matrices where only one component's tag matches).
#
# RC caller (single chart, sugar-suite ci.yaml):
#
# helm-publish:
# needs: [helm-lint, build-sugar-suite, cdn-upload]
# if: github.event_name != 'pull_request' && needs.build-sugar-suite.outputs.rc_version != ''
# uses: bcit-tlu/.github/.github/workflows/helm-publish.yaml@main
# permissions:
# contents: read
# packages: write
# id-token: write
# with:
# chart_dir: charts
# version: ${{ needs.build-sugar-suite.outputs.rc_version }}
# inject_cdn_sha: true
# secrets: inherit
#
# Release caller (monorepo matrix, hriv helm-publish.yaml):
#
# publish:
# strategy:
# matrix:
# component: [frontend, backend, backup]
# uses: bcit-tlu/.github/.github/workflows/helm-publish.yaml@main
# permissions:
# contents: read
# packages: write
# id-token: write
# with:
# chart_dir: charts/${{ matrix.component }}
# tag_name: ${{ inputs.tag_name || github.event.release.tag_name }}
# tag_prefix: ${{ matrix.component }}-v
# ref: ${{ inputs.tag_name || github.event.release.tag_name }}
# update_dependencies: true
# secrets: inherit

name: Helm publish

on:
workflow_call:
inputs:
chart_dir:
description: "Path to the chart directory (e.g. 'charts' or 'charts/frontend')."
required: true
type: string
version:
description: "Explicit chart/app version to publish. When set, tag_name/tag_prefix are ignored."
required: false
type: string
default: ""
tag_name:
description: "Release tag to parse when `version` is empty (e.g. v1.2.3 or backend-v1.2.3)."
required: false
type: string
default: ""
tag_prefix:
description: "Prefix preceding the semver in tag_name (e.g. 'v' or 'backend-v'). A tag not matching is skipped."
required: false
type: string
default: v
ref:
description: "Git ref to checkout (defaults to the triggering commit). Set to the release tag for release publishes."
required: false
type: string
default: ""
inject_cdn_sha:
description: "Inject the short commit SHA into <chart_dir>/values.yaml `cdn.commitSha` before packaging."
required: false
type: boolean
default: false
update_dependencies:
description: "Run `helm dependency update` before packaging (charts with subchart deps)."
required: false
type: boolean
default: false
registry:
description: "OCI registry hosting the chart."
required: false
type: string
default: ghcr.io

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
env:
REGISTRY: ${{ inputs.registry }}
steps:
# Decide what version to publish (and whether to publish at all):
# * explicit `version` wins (RC publishes from ci.yaml);
# * otherwise parse the semver out of `tag_name` using `tag_prefix`;
# * a non-matching tag sets publish=false so the rest of the job is
# skipped (lets monorepo matrices no-op on other components' tags).
- name: Resolve version
id: resolve
shell: bash
env:
VERSION_IN: ${{ inputs.version }}
TAG: ${{ inputs.tag_name }}
PREFIX: ${{ inputs.tag_prefix }}
run: |
set -euo pipefail
if [[ -n "${VERSION_IN}" ]]; then
echo "version=${VERSION_IN}" >> "$GITHUB_OUTPUT"
echo "publish=true" >> "$GITHUB_OUTPUT"
elif [[ "${TAG}" =~ ^${PREFIX}([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
echo "version=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
echo "publish=true" >> "$GITHUB_OUTPUT"
else
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "No version input and tag '${TAG}' does not match prefix '${PREFIX}'; skipping."
fi

# Check out the exact ref being published (release tag for releases,
# triggering commit for RC publishes) so the packaged chart matches it.
- uses: actions/checkout@v7
if: steps.resolve.outputs.publish == 'true'
with:
ref: ${{ inputs.ref || github.sha }}

# `helm` packages/pushes the chart; cosign signs the pushed artifact.
- uses: azure/setup-helm@v5
if: steps.resolve.outputs.publish == 'true'

- uses: sigstore/cosign-installer@v4.1.2
if: steps.resolve.outputs.publish == 'true'

# cosign + helm each need their own OCI login to the registry.
- name: Cosign login (OCI)
if: steps.resolve.outputs.publish == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY: ${{ env.REGISTRY }}
ACTOR: ${{ github.actor }}
run: |
echo "${GITHUB_TOKEN}" | \
cosign login "${REGISTRY}" \
-u "${ACTOR}" \
--password-stdin

# `helm registry login` is separate from the cosign login above.
- name: Helm login (OCI)
if: steps.resolve.outputs.publish == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY: ${{ env.REGISTRY }}
ACTOR: ${{ github.actor }}
run: |
echo "${GITHUB_TOKEN}" | \
helm registry login "${REGISTRY}" \
-u "${ACTOR}" \
--password-stdin

# Package the chart at the resolved version, push it to the OCI registry,
# then keyless-sign the pushed digest with cosign.
- name: Package, push & sign chart
if: steps.resolve.outputs.publish == 'true'
shell: bash
env:
VERSION: ${{ steps.resolve.outputs.version }}
CHART_DIR: ${{ inputs.chart_dir }}
INJECT_CDN_SHA: ${{ inputs.inject_cdn_sha }}
UPDATE_DEPS: ${{ inputs.update_dependencies }}
OCI_BASE: oci://${{ env.REGISTRY }}/${{ github.repository }}/charts
IMAGE_BASE: ${{ env.REGISTRY }}/${{ github.repository }}/charts
run: |
set -euo pipefail

CHART_NAME=$(yq '.name' "${CHART_DIR}/Chart.yaml")
DEST_DIR="/tmp/charts"
mkdir -p "${DEST_DIR}"

# Inject short commit SHA for CDN immutable path (matched by upload prefix).
if [[ "${INJECT_CDN_SHA}" == "true" ]]; then
SHORT_SHA="$(git rev-parse --short HEAD)"
yq -i ".cdn.commitSha = \"${SHORT_SHA}\"" "${CHART_DIR}/values.yaml"
fi

if [[ "${UPDATE_DEPS}" == "true" ]]; then
helm dependency update "${CHART_DIR}" || true
fi

echo "::group::${CHART_NAME} ${VERSION}"
helm package "${CHART_DIR}" \
--version "${VERSION}" \
--app-version "${VERSION}" \
-d "${DEST_DIR}"

PUSH_OUT=$(helm push \
"${DEST_DIR}/${CHART_NAME}-${VERSION}.tgz" \
"${OCI_BASE}" 2>&1) || { echo "${PUSH_OUT}"; exit 1; }
echo "${PUSH_OUT}"

# Tolerant digest parse: case-insensitive, allows leading whitespace.
DIGEST=$(printf '%s\n' "${PUSH_OUT}" \
| awk 'tolower($1)=="digest:"{print $2; exit}')
if [[ -z "${DIGEST}" ]]; then
echo "::error::helm push for ${CHART_NAME} succeeded but no digest found in output (helm output format may have changed)"
exit 1
fi

cosign sign --yes "${IMAGE_BASE}/${CHART_NAME}@${DIGEST}"
echo "::endgroup::"
5 changes: 2 additions & 3 deletions .github/workflows/oci-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ on:

env:
REGISTRY: ghcr.io
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
Expand All @@ -104,7 +103,7 @@ jobs:
actions: read # Required by codeql-action/upload-sarif on private repos

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down Expand Up @@ -245,7 +244,7 @@ jobs:
${{ env.REGISTRY }}/${{ github.repository }}/${{ inputs.image_name }}:${{ steps.version.outputs.next }}-rc.${{ steps.version.outputs.ts }}.${{ steps.version.outputs.short }}

# ── Sign with Cosign (keyless / Sigstore) ────────────────
- uses: sigstore/cosign-installer@v3
- uses: sigstore/cosign-installer@v4.1.2
if: steps.changes.outputs.changed == 'true' && github.event_name != 'pull_request'

- name: Sign image
Expand Down
Loading