Skip to content
Open
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
91 changes: 91 additions & 0 deletions .github/workflows/_quality-gate-candidate-branch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Test and promote a branch's solutions

# Reusable workflow: discovers every solution defined under tests/solution/
# on a single branch, deploys and tests each one, and — only if all of them
# pass — promotes every charm pinned by that branch's Terraform modules from
# /beta to /candidate.
#
# Called once per supported branch/track from quality-gates-candidate.yaml, so adding
# a new track only requires adding an entry to that workflow's branch
# matrix, and adding a new solution only requires adding a directory under
# tests/solution/ (each branch discovers its own set of solutions, so older
# branches without a newer solution are unaffected).

on:
workflow_call:
inputs:
branch:
description: 'The branch/track to check out and test (e.g. "main", "track/3.0").'
required: true
type: string
secrets:
CHARMCRAFT_AUTH:
required: true

jobs:
discover-solutions:
name: Discover solutions
runs-on: ubuntu-latest
outputs:
solutions: ${{ steps.discover.outputs.solutions }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}

- name: List solutions with a Terraform wrapper module
id: discover
run: |
solutions=$(find tests/solution -mindepth 2 -maxdepth 2 -type d -name terraform -printf '%P\n' | sed 's|/terraform$||' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "solutions=$solutions" >> "$GITHUB_OUTPUT"

test-solutions:
name: Test ${{ matrix.solution }}
needs: [discover-solutions]
strategy:
fail-fast: false
matrix:
solution: ${{ fromJson(needs.discover-solutions.outputs.solutions) }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}

- name: Install dependencies
run: |
sudo snap install concierge --classic
sudo concierge prepare -p k8s --extra-snaps just,astral-uv,terraform
# Configure Cilium for Istio ambient service mesh compatibility
sudo k8s kubectl patch configmap cilium-config -n kube-system --type merge -p '{"data":{"bpf-lb-sock-hostns-only":"true"}}'
sudo k8s kubectl rollout restart daemonset/cilium -n kube-system
sudo k8s kubectl rollout status daemonset/cilium -n kube-system --timeout=90s

- name: Run solution tests
run: just quality-gates test-solution ${{ matrix.solution }}

promote:
name: Promote ${{ matrix.solution }}
needs: [discover-solutions, test-solutions]
strategy:
matrix:
solution: ${{ fromJson(needs.discover-solutions.outputs.solutions) }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}

- name: Install dependencies
run: |
sudo snap install terraform --classic
sudo snap install just --classic
sudo snap install charmcraft --classic

- name: Promote
env:
CHARMCRAFT_AUTH: ${{ secrets.CHARMCRAFT_AUTH }}
run: just quality-gates promote-solution ${{ matrix.solution }} ${{ inputs.branch }}
27 changes: 27 additions & 0 deletions .github/workflows/quality-gates-candidate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Solution tests

# Periodically (and on demand) deploy the COS and COS Lite solutions from
# every supported branch and run their solution test suites
# (tests/solution). For each branch, when both solutions pass, promote every
# charm pinned by that branch's Terraform modules from /beta to /candidate.
#
# To support a new branch/track, add it to the `branch` matrix below; a
# separate, independent test+promote pipeline is instantiated per branch.

on:
workflow_dispatch: {}
schedule:
# Once a month, on the 1st at 03:00 UTC.
- cron: "0 3 1 * *"

jobs:
solutions:
name: ${{ matrix.branch }}
strategy:
fail-fast: false
matrix:
branch: [main, track/3.0]
uses: ./.github/workflows/_quality-gate-candidate-branch.yaml
secrets: inherit
with:
branch: ${{ matrix.branch }}
2 changes: 2 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ set export # Just variables are exported to the environment
terraform := `which terraform || which tofu || echo ""` # require 'terraform' or 'opentofu'
uv_flags := "--frozen --isolated"

mod quality-gates

[private]
default:
just --list
Expand Down
70 changes: 70 additions & 0 deletions quality-gates.just
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
set quiet # Recipes are silent by default
set export # Just variables are exported to the environment

terraform := `which terraform || which tofu || echo ""` # require 'terraform' or 'opentofu'
uv_flags := "--frozen --isolated"

[private]
@default:
just --list quality-gates

# Init/apply a solution's Terraform module, then run its solution test suite
[group("test")]
[working-directory("./tests/solution")]
test-solution solution="cos-lite":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the promote tests, we likely want to be passing the risk to the module in order to really test the beta risk here. I think we don't do that yet?

#!/usr/bin/env bash
set -euo pipefail
echo "==> Testing solution: {{solution}}"
if [ -z "${terraform}" ]; then echo "ERROR: please install terraform or opentofu"; exit 1; fi
$terraform -chdir="{{solution}}/terraform" init -upgrade
$terraform -chdir="{{solution}}/terraform" apply -auto-approve
uv run ${uv_flags} pytest -vv -ra --capture=no "{{solution}}"

# Promote every charm in a solution from /beta to /candidate
[group("promote")]
promote-solution solution="cos-lite" ref="main":
#!/usr/bin/env bash
set -euo pipefail
echo "==> Promoting solution: {{solution}} (ref: {{ref}})"
charms_tracks=$(just charm-tracks "{{solution}}" "{{ref}}")
if [ -z "$charms_tracks" ]; then
echo "No pinned charms found for solution '{{solution}}'; nothing to promote."
exit 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this shouldn't exit with a non-zero exit code. What are the odds we'll trigger promotion but won't actually want to promote anything?

fi
while read -r charm track; do
echo "--> Promoting $charm from ${track}/beta to ${track}/candidate"
charmcraft promote --name "$charm" --from-channel "${track}/beta" --to-channel "${track}/candidate" --yes
done <<< "$charms_tracks"

# List the charm name + track pinned by each charm in a product module
[group("promote")]
charm-tracks module ref='main':
#!/usr/bin/env bash
set -euo pipefail
excluded_charms="self-signed-certificates traefik-k8s s3-integrator"
upgrades=$(git show "{{ref}}:terraform/{{module}}/upgrades.tf")
locals=$(git show "{{ref}}:terraform/{{module}}/locals.tf")

charm_keys=$(awk '
/data[[:space:]]*"juju_charm"[[:space:]]*"/ { in_block=1; next }
in_block && /^\}/ { in_block=0; charm=""; key=""; next }
in_block && match($0, /charm[[:space:]]*=[[:space:]]*"([^"]*)"/, m) { charm=m[1] }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth checking if the matchers will work with github runners. We might be using mawk there, this might not support the three-param match.

in_block && match($0, /channel[[:space:]]*=[[:space:]]*local\.channels\.([a-zA-Z0-9_]+)/, m) { key=m[1] }
in_block && charm != "" && key != "" { print charm, key; charm=""; key="" }
' <<< "$upgrades")

declare -A tracks
while read -r tkey tval; do
[ -n "$tkey" ] && tracks["$tkey"]="$tval"
done < <(awk '
/tracks[[:space:]]*=[[:space:]]*\{/ { in_tracks=1; next }
in_tracks && /^ \}/ { in_tracks=0; next }
in_tracks && match($0, /^[[:space:]]*([a-zA-Z0-9_]+)[[:space:]]*=[[:space:]]*"([^"]*)"/, m) { print m[1], m[2] }
' <<< "$locals")

while read -r charm key; do
[ -z "$charm" ] && continue
case " $excluded_charms " in *" $charm "*) continue ;; esac
track="${tracks[$key]:-}"
[ -n "$track" ] && printf '%s %s\n' "$charm" "$track"
done <<< "$charm_keys"
14 changes: 14 additions & 0 deletions tests/solution/cos-lite/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.5"
required_providers {
juju = {
source = "juju/juju"
version = ">= 1.0"
}
}
}

# All defaults: creates its own model ("cos-lite"), edge risk, self-signed TLS.
module "cos-lite" {
source = "../../../../terraform/cos-lite"
}
14 changes: 14 additions & 0 deletions tests/solution/cos-lite/test_solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Copyright 2025 Canonical Ltd.
See LICENSE file for licensing details.

Placeholder solution test for COS Lite.

TODO: replace with real assertions against the deployed solution (e.g. via
jubilant). Feature files / BDD steps / helpers can be introduced later; this
smoke test only confirms the test harness (terraform apply -> pytest) wires
up correctly.
"""


def test_solution_test_harness_runs():
assert True
35 changes: 35 additions & 0 deletions tests/solution/cos/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
terraform {
required_version = ">= 1.5"
required_providers {
juju = {
source = "juju/juju"
version = ">= 1.0"
}
}
}

resource "juju_model" "cos" {
name = "cos"
}

# SeaweedFS provides an in-cluster S3-compatible store, avoiding the need for
# external S3 credentials/infrastructure in this smoke test. Its S3 gateway
# uses fixed placeholder credentials (see the seaweedfs-k8s charm source),
# and is reachable at its Juju unit's in-cluster Kubernetes service address.
module "seaweedfs" {
source = "../../../../terraform/seaweedfs"
model_uuid = juju_model.cos.uuid
}

# All other defaults: edge risk, self-signed internal TLS (no external CA
# needed).
module "cos" {
source = "../../../../terraform/cos"
depends_on = [module.seaweedfs]

model = { uuid = juju_model.cos.uuid }

s3_endpoint = "http://${module.seaweedfs.app_name}.cos.svc.cluster.local:8333"
s3_access_key = "placeholder"
s3_secret_key = "placeholder"
}
14 changes: 14 additions & 0 deletions tests/solution/cos/test_solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Copyright 2025 Canonical Ltd.
See LICENSE file for licensing details.

Placeholder solution test for COS.

TODO: replace with real assertions against the deployed solution (e.g. via
jubilant). Feature files / BDD steps / helpers can be introduced later; this
smoke test only confirms the test harness (terraform apply -> pytest) wires
up correctly.
"""


def test_solution_test_harness_runs():
assert True
Loading