Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b4faa67
feat: add kubernetes runner prototype
hllvc Oct 12, 2026
97b9403
feat: size and gracefully drain k8s runners
hllvc Oct 12, 2026
02c7bd8
feat: park k8s runners and drain them through sg
hllvc Oct 13, 2026
071969e
feat: autoscale k8s runners with a scaler
hllvc Oct 13, 2026
753aaab
feat: park idle runners after 1m, deregister after 12h
hllvc Oct 13, 2026
2f28ea6
fix: detect the private ip from the outbound route
hllvc Oct 13, 2026
e8c21e1
feat: pin k8s image versions and add a multi-arch build
hllvc Oct 13, 2026
a6d7d83
feat: prune the nested docker from the k8s dockerd container
hllvc Oct 13, 2026
40e05ed
feat: test and lint k8s runner.sh
hllvc Oct 13, 2026
d668ac0
feat: deploy k8s runners with a helm chart
hllvc Oct 13, 2026
0774c47
Merge branch 'feat/k8s-runtime' into feat/k8s-prototype
hllvc Oct 13, 2026
7c33c48
feat: shape the chart's scaler for its own non-root image
hllvc Oct 13, 2026
66f809b
feat: pass reap job pod settings and metrics address to the scaler
hllvc Oct 13, 2026
b491f5f
feat: port the scaler rules to go, with an sg api client
hllvc Oct 13, 2026
f753c53
Merge branch 'feat/k8s-helm-chart' into feat/k8s-prototype
hllvc Oct 13, 2026
234e82a
feat: run the go scaler loop with events, metrics, leader election an…
hllvc Oct 13, 2026
2f644f7
feat: build the go scaler image
hllvc Oct 13, 2026
3ee215a
fix: count a runner reactivated this loop as active
hllvc Oct 13, 2026
5e06a9d
feat: replace scaler.sh with the go scaler
hllvc Oct 13, 2026
4be94a6
fix: let the scaler list reap jobs
hllvc Oct 13, 2026
862e108
feat: configure reap job labels, pull secrets and scheduling
hllvc Oct 13, 2026
6ee0f1e
feat: test reap job settings with the chart's values
hllvc Oct 13, 2026
ad2c2e3
Merge branch 'feat/k8s-scaler-go' into feat/k8s-prototype
hllvc Oct 13, 2026
fcd4b42
docs: drop the last scaler.sh mention from the chart
hllvc Oct 13, 2026
ee49eee
docs: describe the chart, go scaler and runner sizes
hllvc Oct 13, 2026
59b8fca
feat: add k8s CI for the scaler, chart, images and a kind smoke install
hllvc Oct 13, 2026
1292675
fix: stop error reports from retrying themselves when the api is down
hllvc Oct 13, 2026
d5f0a66
Merge branch 'feat/k8s-prototype' into feat/k8s-ci
hllvc Oct 13, 2026
f19cb70
fix: pick up debian security updates in the runner image
hllvc Oct 13, 2026
6ba8ef6
feat: make the kind smoke strict about register failing cleanly
hllvc Oct 13, 2026
1e37835
Merge branch 'feat/k8s-ci' into feat/k8s-prototype
hllvc Oct 13, 2026
9203eab
feat: scaler teardown mode, autoscaler nudge and config from a configmap
hllvc Oct 14, 2026
ff1f476
feat: workflow logs sidecar and reap without a registration
hllvc Oct 14, 2026
a818826
feat: uninstall hook, token-only secret, nudge and workflow logs in t…
hllvc Oct 14, 2026
80d7e16
docs: k8s docs for gke, uninstall, nudge and native steps
hllvc Oct 14, 2026
073c1e5
feat: step broker that runs workflow steps as pods
hllvc Oct 14, 2026
4e5907e
feat: re-register a runner when its advertised size changes
hllvc Oct 14, 2026
2712772
feat: native steps in the chart
hllvc Oct 14, 2026
1967e65
feat: native steps stage in the kind smoke
hllvc Oct 14, 2026
3274ff0
docs: native steps design, broker api and runbook
hllvc Oct 14, 2026
9098891
feat: step wrapper so exits 11 and 12 end completed
hllvc Oct 14, 2026
0ea9f8f
feat: step wrapper, outcome label and retention in the chart
hllvc Oct 14, 2026
76194b0
docs: step wrapper, outcome label and retention
hllvc Oct 14, 2026
3734ee9
feat: dockerd guard against step containers and the broker as a sidecar
hllvc Oct 14, 2026
58a699d
feat: native-only kubernetes runner
hllvc Oct 14, 2026
0acb252
docs: native-only runner, guard and requirements
hllvc Oct 14, 2026
7964b56
docs: describe native steps in the kubernetes readme section
hllvc Oct 14, 2026
bf01800
feat: publish the kubernetes runner images and chart to ghcr on relea…
hllvc Oct 14, 2026
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
268 changes: 268 additions & 0 deletions .github/scripts/chart-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
#!/usr/bin/env bash
# Lints the sg-runner chart, renders the key value scenarios, validates every
# rendered manifest with kubeconform and checks that the schema and the
# templates reject bad values. Needs helm (v3 or v4) and kubeconform on PATH;
# never talks to a cluster.
#
# .github/scripts/chart-check.sh [out-dir]
#
# KUBE_VERSION: Kubernetes version the manifests are validated against.
set -euo pipefail

CHART=k8s/chart/sg-runner
OUT=${1:-${RUNNER_TEMP:-/tmp}/chart-check}
KUBE_VERSION=${KUBE_VERSION:-1.37.0}
# Release name = chart name, as the k8s/Makefile installs it.
RELEASE=sg-runner
# Every scenario needs credentials; the Secret doesn't have to exist to render.
CREDS=(--set credentials.existingSecret=sg-runner-credentials
--set sg.organization=ci-org --set sg.runnerGroup=ci-group)

rm -rf "$OUT" && mkdir -p "$OUT"
failed=0

render() { # <name> <helm template args...>
local name=$1
shift
echo "::group::render $name"
if helm template "$RELEASE" "$CHART" --namespace sg-runner "$@" >"$OUT/$name.yaml"; then
echo "rendered $(grep -c '^kind:' "$OUT/$name.yaml") objects"
else
echo "::error title=helm template::scenario '$name' failed to render"

Check warning on line 31 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQE2&open=AaE8qvwh85mCeENibQE2&pullRequest=40
failed=1
fi
echo "::endgroup::"
}

# Rendering must fail, and with the expected message (so an unrelated error
# doesn't count as the rejection).
reject() { # <name> <expected stderr regex> <helm template args...>
local name=$1 want=$2
shift 2
if helm template "$RELEASE" "$CHART" "$@" >/dev/null 2>"$OUT/$name.err"; then
echo "::error title=helm template::scenario '$name' rendered, expected a rejection"

Check warning on line 43 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQE3&open=AaE8qvwh85mCeENibQE3&pullRequest=40
failed=1
elif ! grep -Eq "$want" "$OUT/$name.err"; then
echo "::error title=helm template::scenario '$name' failed for another reason:"

Check warning on line 46 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQE4&open=AaE8qvwh85mCeENibQE4&pullRequest=40
cat "$OUT/$name.err"
failed=1
else
echo "rejected as expected: $name ($(head -c 200 "$OUT/$name.err" | tr '\n' ' '))"
fi
}

# Must contain the given kind (guards against a scenario silently rendering
# nothing new).
expect_kind() { # <name> <kind>
grep -q "^kind: $2\$" "$OUT/$1.yaml" \

Check warning on line 57 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQE5&open=AaE8qvwh85mCeENibQE5&pullRequest=40

Check warning on line 57 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQE6&open=AaE8qvwh85mCeENibQE6&pullRequest=40
|| { echo "::error title=helm template::scenario '$1' has no $2"; failed=1; }
}
expect_no_kind() { # <name> <kind>
! grep -q "^kind: $2\$" "$OUT/$1.yaml" \

Check warning on line 61 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQE9&open=AaE8qvwh85mCeENibQE9&pullRequest=40
|| { echo "::error title=helm template::scenario '$1' unexpectedly has a $2"; failed=1; }
}
expect_text() { # <name> <fixed string>
grep -qF -- "$2" "$OUT/$1.yaml" \

Check warning on line 65 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFA&open=AaE8qvwh85mCeENibQFA&pullRequest=40
|| { echo "::error title=helm template::scenario '$1' lacks: $2"; failed=1; }
}
expect_count() { # <name> <fixed string> <count>
local got
got=$(grep -cF -- "$2" "$OUT/$1.yaml" || true)
[[ $got == "$3" ]] \

Check warning on line 71 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFC&open=AaE8qvwh85mCeENibQFC&pullRequest=40
|| { echo "::error title=helm template::scenario '$1' has $got x '$2', want $3"; failed=1; }

Check warning on line 72 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFD&open=AaE8qvwh85mCeENibQFD&pullRequest=40
}

echo "::group::helm lint --strict"
helm version
helm lint --strict "$CHART" "${CREDS[@]}" || failed=1
echo "::endgroup::"

render defaults "${CREDS[@]}"
expect_kind defaults StatefulSet
expect_kind defaults Deployment
expect_kind defaults Role
# The uninstall hook (pre-delete Job).
expect_kind defaults Job
# The register init container sees what ecs-agent will advertise, to
# re-register a runner whose size changed.
expect_count defaults 'name: SG_TASK_CPU_LIMIT' 2
expect_count defaults 'name: SG_TASK_MEMORY_LIMIT_MIB' 2
expect_text defaults '{ name: SG_REREGISTER_ON_RESIZE, value: "true" }'

render credentials-create \
--set credentials.create=true \
--set credentials.nodeToken=ci-dummy-token \
--set sg.organization=ci-org \
--set sg.runnerGroup=ci-group \
--set sg.apiUrl=http://127.0.0.1:9
expect_kind credentials-create Secret
expect_kind credentials-create ConfigMap

render scaler-disabled "${CREDS[@]}" --set scaler.enabled=false --set runner.replicas=2 \
--set uninstall.deregister=false
expect_no_kind scaler-disabled Deployment
# Only the runner's Role (the steps broker), not the scaler's.
[[ $(grep -c '^kind: Role$' "$OUT/scaler-disabled.yaml") == 1 ]] \
|| { echo "::error title=helm template::scenario 'scaler-disabled' should have only the runner's Role"; failed=1; }

Check warning on line 106 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFE&open=AaE8qvwh85mCeENibQFE&pullRequest=40
expect_no_kind scaler-disabled Job

# Without the scaler, the uninstall hook still cleans up, with its own Role.
render uninstall-without-scaler "${CREDS[@]}" --set scaler.enabled=false
expect_no_kind uninstall-without-scaler Deployment
expect_kind uninstall-without-scaler Job
expect_kind uninstall-without-scaler Role

render network-policy "${CREDS[@]}" \
--set networkPolicy.enabled=true \
--set 'networkPolicy.clusterCIDRs={10.0.0.0/8,172.16.0.0/12}'
expect_kind network-policy NetworkPolicy

# The runner pod's init containers (register, then the steps sidecar).
init_containers() { # <name>
awk '/^kind: StatefulSet$/ {sts = 1} sts && /^ initContainers:$/ {on = 1; next}
on && /^ containers:$/ {exit} on' "$OUT/$1.yaml"
}

# Defaults: size small, the broker a native sidecar guarding dockerd, the
# runner's RBAC, a workspace PVC per pod, one runner per node.
expect_kind defaults ServiceAccount
expect_kind defaults RoleBinding
expect_text defaults 'volumeClaimTemplate:'
expect_text defaults 'topologyKey: kubernetes.io/hostname'
expect_count defaults '{ name: SG_TASK_CPU_LIMIT, value: "2" }' 2
expect_count defaults '{ name: SG_TASK_MEMORY_LIMIT_MIB, value: "4096" }' 2
expect_count defaults '{ name: SG_DOCKERD_OVERHEAD_MIB, value: "0" }' 2
expect_text defaults 'verbs: ["create", "get", "list", "watch", "patch", "delete"]'
# Published images by default, tagged with the chart's appVersion ("dev" in
# git; the release workflow sets the version).
expect_text defaults 'image: ghcr.io/stackguardian/sg-runner-k8s:dev'
expect_text defaults 'image: ghcr.io/stackguardian/sg-runner-scaler:dev'
expect_text defaults '{ name: BROKER_IMAGE, value: "ghcr.io/stackguardian/sg-runner-scaler:dev" }'
expect_text defaults '{ name: SCALER_IMAGE, value: "ghcr.io/stackguardian/sg-runner-k8s:dev" }'
expect_text defaults '{ name: BROKER_IMAGE_PULL_POLICY, value: "IfNotPresent" }'
expect_text defaults '{ name: FINISHED_TTL, value: "1h" }'
expect_text defaults '{ name: GUARD_SOCKET, value: /run/sg-guard/sg-steps-guard.sock }'
expect_text defaults '{ name: docker-plugins, mountPath: /run/docker/plugins }'
expect_text defaults '{ name: docker-plugins, mountPath: /run/sg-guard }'
# The sidecar outlives the drain by itself: no drain marker, no preStop.
expect_count defaults 'DRAINED_MARKER' 0
expect_count defaults "command: ['/sg-runner-steps', 'wait-drained']" 0
expect_count defaults 'timeoutSeconds: 6' 2
if ! init_containers defaults | grep -q -- '- name: steps' \
|| ! init_containers defaults | grep -q 'restartPolicy: Always' \
|| ! init_containers defaults | grep -qF "command: ['/sg-runner-steps', 'healthcheck']"; then
echo "::error title=helm template::scenario 'defaults': the steps broker isn't a native sidecar with a healthcheck"

Check warning on line 154 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFG&open=AaE8qvwh85mCeENibQFG&pullRequest=40
failed=1
fi
if grep -q 'workflow-logs' "$OUT/defaults.yaml"; then
echo "::error title=helm template::scenario 'defaults' still has workflow-logs"
failed=1
fi
if [[ $(grep -c 'mountPath: /var/run/secrets/kubernetes.io/serviceaccount' "$OUT/defaults.yaml") -ne 1 ]]; then
echo "::error title=helm template::scenario 'defaults' must mount the API token in exactly one container"

Check warning on line 162 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFI&open=AaE8qvwh85mCeENibQFI&pullRequest=40
failed=1
fi

# Presets set what the runner advertises and the controllers' room.
render size-large "${CREDS[@]}" --set runner.size=large
expect_count size-large '{ name: SG_TASK_CPU_LIMIT, value: "8" }' 2
expect_count size-large '{ name: SG_TASK_MEMORY_LIMIT_MIB, value: "16384" }' 2
expect_text size-large 'memory: 8Gi'

render size-custom "${CREDS[@]}" \
--set runner.size=custom \
--set runner.advertise.cpu=3 --set runner.advertise.memory=3072Mi \
--set runner.controllerResources.requests.cpu=250m \
--set runner.controllerResources.requests.memory=512Mi \
--set runner.controllerResources.limits.cpu=1 \
--set runner.controllerResources.limits.memory=1536Mi
expect_count size-custom '{ name: SG_TASK_CPU_LIMIT, value: "3" }' 2
expect_count size-custom '{ name: SG_TASK_MEMORY_LIMIT_MIB, value: "3072" }' 2
expect_text size-custom 'memory: 1536Mi'

# Every knob at once; the NetworkPolicy covers step pods.
render everything "${CREDS[@]}" --set runner.size=medium \
--set networkPolicy.enabled=true --set 'networkPolicy.clusterCIDRs={10.0.0.0/8}' \
--set 'runner.ecsAgent.extraEnv[0].name=ECS_IMAGE_PULL_BEHAVIOR' \
--set 'runner.ecsAgent.extraEnv[0].value=prefer-cached' \
--set 'runner.dockerd.extraEnv[0].name=SG_PRUNE_INTERVAL_SECONDS' \
--set-string 'runner.dockerd.extraEnv[0].value=0' \
--set runner.steps.step.imagePullPolicy=IfNotPresent \
--set 'imagePullSecrets[0].name=regcred' --set 'imagePullSecrets[1].name=other' \
--set runner.reregisterOnResize=false \
--set 'runner.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution[0].topologyKey=zone'
expect_kind everything NetworkPolicy
expect_text everything 'values: [runner, reaper, step]'
expect_count everything '{ name: SG_TASK_CPU_LIMIT, value: "4" }' 2
expect_text everything '{ name: BROKER_IMAGE_PULL_SECRETS, value: "regcred,other" }'
expect_text everything '{ name: STEP_IMAGE_PULL_POLICY, value: IfNotPresent }'
expect_text everything 'name: SG_PRUNE_INTERVAL_SECONDS'
expect_text everything 'name: ECS_IMAGE_PULL_BEHAVIOR'
expect_text everything '{ name: SG_REREGISTER_ON_RESIZE, value: "false" }'
expect_text everything 'topologyKey: zone'
expect_text everything 'topologyKey: kubernetes.io/hostname'

render pdb "${CREDS[@]}" --set podDisruptionBudget.enabled=true --set scaler.min=2
expect_kind pdb PodDisruptionBudget

# --api-versions makes the ServiceMonitor render, as on a cluster with the
# Prometheus Operator CRDs.
render metrics "${CREDS[@]}" \
--set scaler.metrics.enabled=true \
--set scaler.metrics.serviceMonitor.enabled=true \
--api-versions monitoring.coreos.com/v1/ServiceMonitor
expect_kind metrics Service
expect_kind metrics ServiceMonitor

# imagePullSecrets as plain names and {name:} objects, mixed: every pod gets
# {name:} refs, the scaler and the broker the names.
render pull-secrets-mixed "${CREDS[@]}" \
--set-json 'imagePullSecrets=["plain",{"name":"obj"}]'
expect_count pull-secrets-mixed '- name: plain' 3
expect_count pull-secrets-mixed '- name: obj' 3
expect_text pull-secrets-mixed '{ name: BROKER_IMAGE_PULL_SECRETS, value: "plain,obj" }'
expect_count pull-secrets-mixed 'value: "plain,obj"' 3

echo "::group::kubeconform (Kubernetes $KUBE_VERSION)"
# ServiceMonitor is validated against the community CRDs catalog.
kubeconform -strict -summary -output text \
-kubernetes-version "$KUBE_VERSION" \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
"$OUT"/*.yaml || failed=1
echo "::endgroup::"

echo "::group::rejections"
reject size-huge 'runner[./]size' "${CREDS[@]}" --set runner.size=huge
reject scaler-min-0 'scaler[./]min' "${CREDS[@]}" --set scaler.min=0
reject no-credentials 'credentials' --set sg.organization=o --set sg.runnerGroup=g
reject both-credentials 'not both' "${CREDS[@]}" --set credentials.create=true
reject no-runner-group 'sg.organization and sg.runnerGroup' \
--set credentials.existingSecret=x --set sg.organization=o
reject token-only-secret 'additional properties|additionalProperties|organization' \
--set credentials.existingSecret=x --set credentials.organization=o \
--set sg.organization=o --set sg.runnerGroup=g
reject network-policy-no-cidrs 'clusterCIDRs' "${CREDS[@]}" --set networkPolicy.enabled=true
reject custom-without-advertise 'advertise' "${CREDS[@]}" --set runner.size=custom \
--set runner.controllerResources.limits.cpu=1 --set runner.controllerResources.limits.memory=1Gi
reject custom-without-controllers 'controllerResources' "${CREDS[@]}" --set runner.size=custom \
--set runner.advertise.cpu=2 --set runner.advertise.memory=4Gi
reject fractional-cpu 'advertise[./]cpu' "${CREDS[@]}" --set runner.size=custom \
--set runner.advertise.cpu=1.5 --set runner.advertise.memory=4Gi \
--set runner.controllerResources.limits.cpu=1 --set runner.controllerResources.limits.memory=1Gi
reject memory-bytes 'advertise[./]memory' "${CREDS[@]}" --set runner.size=custom \
--set runner.advertise.cpu=2 --set runner.advertise.memory=6442450944 \
--set runner.controllerResources.limits.cpu=1 --set runner.controllerResources.limits.memory=1Gi
reject bad-timeout 'startTimeout' "${CREDS[@]}" --set runner.steps.startTimeout=30
reject bad-pull-policy 'imagePullPolicy' "${CREDS[@]}" --set runner.steps.step.imagePullPolicy=Sometimes
reject old-native-steps 'nativeSteps|additional' "${CREDS[@]}" --set runner.nativeSteps.enabled=true
reject pull-secret-empty-name 'imagePullSecrets' "${CREDS[@]}" --set-json 'imagePullSecrets=[{"name":""}]'
reject replicas-without-lease 'leaderElection' "${CREDS[@]}" \
--set scaler.replicas=2 --set scaler.leaderElection.enabled=false
echo "::endgroup::"

Check warning on line 262 in .github/scripts/chart-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using the literal '::endgroup::' 4 times.

See more on https://sonarcloud.io/project/issues?id=StackGuardian_sg-runner&issues=AaE8qvwh85mCeENibQFJ&open=AaE8qvwh85mCeENibQFJ&pullRequest=40

if [[ $failed -ne 0 ]]; then
echo "chart check failed"
exit 1
fi
echo "chart check passed"
Loading
Loading