-
Notifications
You must be signed in to change notification settings - Fork 12
feat: add candidate quality gates for solution promotion #477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }} |
| 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 }} |
| 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": | ||
| #!/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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
| 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" | ||
| } |
| 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 |
| 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" | ||
| } |
| 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 |
There was a problem hiding this comment.
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
betarisk here. I think we don't do that yet?