From f05f089974d8e77947357771c0953bccd59423aa Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Sun, 7 Jun 2026 12:41:21 +0530 Subject: [PATCH 1/6] feat: Phase 1 auth bootstrap + test infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add automated auth bootstrap (no browser OAuth required), integration test lifecycle tasks, CI workflow, and correct test tags. ## Auth bootstrap (scripts/bootstrap.sh) Implements a fully automated Keycloak ROPC flow: 1. Wait for Keycloak health 2. Obtain admin token via master realm 3. Create smoke-test-user (idempotent — 409 is OK) 4. Request offline token via Resource Owner Password Credentials grant using the new smoke-test-client OIDC client (directAccessGrants=true) 5. Wait for Minder server health 6. Call to trigger user auto-enrollment 7. Extract and persist the root project ID Eliminates the need for any browser interaction or GitHub OAuth. ## Integration test tasks (Taskfile.yml) - integration-setup: starts minder docker stack + runs bootstrap.sh - integration-run: executes the Robot Framework suite - integration-teardown: docker compose down + cleanup - integration-test: full lifecycle (setup → run → teardown with defer) ## CI workflow (.github/workflows/integration-tests.yml) Runs on: workflow_dispatch, weekly schedule (Mon 06:00 UTC), and PRs touching test infrastructure files. Steps: checkout both repos, build Minder, start run-docker stack, health-check, bootstrap, run tests, upload results, teardown. ## Test tags Add correct tags to all test files so -i core and -e github-required work as documented in the README: - api-tests.robot, api-profiles.robot, api-datasources.robot, api-project.robot, api-provider.robot: tagged smoke + core - api-repositories.robot, api-history.robot: tagged github-required + provider-required (unchanged, already had these tags) - minder-tests.robot: Valid login → smoke+core, Project created → smoke+core, Provider enrolled → smoke+github-required+provider-required (cannot pass without DB-seeded provider, Phase 2) ## Configs and compose - smoke-test-config.yaml: host-side config (localhost ports) - smoke-test-config-docker.yaml: in-network config (Docker service names) - docker-compose.test.yml: sidecar compose for running tests alongside the Minder stack ## Note on Keycloak client bootstrap.sh now defaults to CLIENT_ID=smoke-test-client. A companion PR to mindersec/minder is required to add this client to the Keycloak realm JSON with directAccessGrantsEnabled: true. --- .github/workflows/integration-tests.yml | 192 ++++++++++++++++++++++ .gitignore | 2 + README.md | 95 +++++++++++ Taskfile.yml | 72 +++++++++ docker-compose.test.yml | 43 +++++ minder-tests/api-datasources.robot | 2 + minder-tests/api-history.robot | 2 + minder-tests/api-profiles.robot | 2 + minder-tests/api-project.robot | 2 + minder-tests/api-provider.robot | 2 + minder-tests/api-repositories.robot | 2 + minder-tests/api-tests.robot | 2 + minder-tests/minder-tests.robot | 6 +- scripts/bootstrap.sh | 201 ++++++++++++++++++++++++ smoke-test-config-docker.yaml | 22 +++ smoke-test-config.yaml | 25 +++ 16 files changed, 670 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 docker-compose.test.yml create mode 100755 scripts/bootstrap.sh create mode 100644 smoke-test-config-docker.yaml create mode 100644 smoke-test-config.yaml diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..f27a8e0 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,192 @@ +--- +name: Integration Tests + +on: + # Manual trigger for on-demand runs + workflow_dispatch: + inputs: + minder_ref: + description: 'Minder branch/tag to test against' + required: false + default: 'main' + test_tags: + description: 'Robot Framework tag filter (e.g. "smoke", "core")' + required: false + default: '' + + # Weekly scheduled run + schedule: + - cron: '0 6 * * 1' # Every Monday at 6:00 AM UTC + + # Run on PRs to validate test infrastructure changes + pull_request: + paths: + - 'scripts/**' + - 'docker-compose.test.yml' + - 'smoke-test-config*.yaml' + - '.github/workflows/integration-tests.yml' + - 'Dockerfile' + - 'requirements.txt' + +concurrency: + group: integration-tests-${{ github.ref }} + cancel-in-progress: true + +env: + MINDER_REF: ${{ github.event.inputs.minder_ref || 'main' }} + +jobs: + integration-test: + name: Run Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + permissions: + contents: read + id-token: write # For potential OIDC use in the future + + steps: + # ------------------------------------------------------------------ + # Checkout + # ------------------------------------------------------------------ + - name: Checkout smoke-tests + uses: actions/checkout@v4 + + - name: Checkout minder + uses: actions/checkout@v4 + with: + repository: mindersec/minder + ref: ${{ env.MINDER_REF }} + path: minder + + # ------------------------------------------------------------------ + # Dependencies + # ------------------------------------------------------------------ + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache-dependency-path: minder/go.sum + + - name: Install ko + uses: ko-build/setup-ko@v0.6 + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq \ + https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + + # ------------------------------------------------------------------ + # Build Minder + # ------------------------------------------------------------------ + - name: Bootstrap Minder + working-directory: minder + run: make bootstrap + + - name: Build Minder + working-directory: minder + run: make build + + # ------------------------------------------------------------------ + # Start Minder stack + # ------------------------------------------------------------------ + - name: Start Minder Docker stack + working-directory: minder + run: make run-docker + + - name: Wait for Minder stack health + run: | + echo "Waiting for Minder server to be healthy..." + timeout 180 bash -c ' + until curl -sf http://localhost:8080/api/v1/health > /dev/null 2>&1; do + echo " ...waiting" + sleep 5 + done + ' + echo "Minder server is healthy!" + + echo "Waiting for Keycloak..." + timeout 120 bash -c ' + until curl -sf http://localhost:8081/health/ready > /dev/null 2>&1; do + echo " ...waiting" + sleep 5 + done + ' + echo "Keycloak is healthy!" + + # ------------------------------------------------------------------ + # Bootstrap test user + # ------------------------------------------------------------------ + - name: Bootstrap test user and project + run: ./scripts/bootstrap.sh + env: + KEYCLOAK_URL: http://localhost:8081 + MINDER_API_URL: http://localhost:8080 + MINDER_BINARY: ${{ github.workspace }}/minder/bin/minder + OFFLINE_TOKEN_OUTPUT_PATH: ${{ github.workspace }}/offline.token + CLIENT_ID: smoke-test-client + + # ------------------------------------------------------------------ + # Run tests + # ------------------------------------------------------------------ + - name: Build smoke test image + run: task build + + - name: Run smoke tests + run: | + EXTRA_ARGS="" + if [ -n "${{ github.event.inputs.test_tags }}" ]; then + EXTRA_ARGS="-i ${{ github.event.inputs.test_tags }}" + fi + task test -- ${EXTRA_ARGS} + env: + MINDER_CONFIG: ${{ github.workspace }}/smoke-test-config.yaml + MINDER_OFFLINE_TOKEN_PATH: ${{ github.workspace }}/offline.token + MINDER_BINARY_PATH: ${{ github.workspace }}/minder/bin/minder + MINDER_TEST_ORG: ${{ secrets.MINDER_TEST_ORG }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + + # ------------------------------------------------------------------ + # Results + # ------------------------------------------------------------------ + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: robot-test-results + path: | + results/ + retention-days: 30 + + - name: Publish test report + if: always() + uses: dorny/test-reporter@v1 + with: + name: 'Integration Test Results' + path: results/xoutput.xml + reporter: java-junit + fail-on-error: true + + # ------------------------------------------------------------------ + # Teardown + # ------------------------------------------------------------------ + - name: Show Minder logs on failure + if: failure() + working-directory: minder + run: docker compose logs minder --tail=100 + + - name: Show Keycloak logs on failure + if: failure() + working-directory: minder + run: docker compose logs keycloak --tail=50 + + - name: Teardown Minder stack + if: always() + working-directory: minder + run: docker compose down -v --remove-orphans diff --git a/.gitignore b/.gitignore index b660548..6c6ae02 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ config.yaml log.html output.xml report.html +# integration test generated files +minder-project-id diff --git a/README.md b/README.md index d36cc06..a1904ec 100644 --- a/README.md +++ b/README.md @@ -155,3 +155,98 @@ Test with repo Given a copy of repo stacklok/demo-repo-python ${test_repo} Then assert stuff on ${test_repo} ``` + +### Running integration tests against a local Minder stack + +The integration test mode runs the smoke tests against a full Minder stack +started via `docker compose` (from the main +[mindersec/minder](https://github.com/mindersec/minder) repo). + +#### Prerequisites + +- Docker / Podman +- [Task](https://taskfile.dev/#/installation) +- A cloned copy of the `mindersec/minder` repository +- `curl`, `jq` on the host +- The `minder` CLI binary on PATH (or set `MINDER_BINARY`) +- A `smoke-test-client` OIDC client in the Keycloak realm with **Direct Access Grants (ROPC) enabled**. + This client must be added to the Keycloak realm JSON in the `mindersec/minder` repo + (`deploy/k8s/keycloak/` or the `run-docker` realm import file) before the bootstrap script + will succeed. See the bootstrap script header for details. + +#### Quick start + +```bash +# 1. Clone minder next to the smoke-tests repo (or set MINDER_REPO_PATH) +git clone git@github.com:mindersec/minder.git ../minder + +# 2. Run the full integration test lifecycle +task integration-test +``` + +This will: +1. Start the Minder Docker stack (`docker compose up`) +2. Run `scripts/bootstrap.sh` to create a test user in Keycloak and generate + an offline token (no browser interaction needed) +3. Execute the Robot Framework test suite +4. Tear down the Docker stack + +#### Running tests manually (step by step) + +```bash +# Start Minder stack +task integration-setup + +# Run tests (can be repeated without re-setup) +task integration-run + +# Optionally, run only core tests (no GitHub provider needed): +MINDER_CONFIG=$(pwd)/smoke-test-config.yaml \ + MINDER_OFFLINE_TOKEN_PATH=$(pwd)/offline.token \ + task test -- -i core + +# Tear down +task integration-teardown +``` + +#### Test tags + +Tests are tagged by their infrastructure requirements: + +| Tag | Meaning | +|-----|---------| +| `smoke` | All smoke tests (default) | +| `core` | Tests that only need Minder API + auth token (no GitHub) | +| `login` | Authentication / whoami tests | +| `github-required` | Tests that need a live GitHub org and token | +| `provider-required` | Tests that need an enrolled GitHub App provider | + +To run only core tests: +```bash +task test -- -i core +``` + +To exclude GitHub-dependent tests: +```bash +task test -- -e github-required +``` + +#### Environment variables for integration mode + +| Variable | Default | Description | +|----------|---------|-------------| +| `MINDER_REPO_PATH` | `../minder` | Path to cloned `mindersec/minder` repo | +| `KEYCLOAK_URL` | `http://localhost:8081` | Keycloak base URL | +| `MINDER_API_URL` | `http://localhost:8080` | Minder HTTP API URL | +| `MINDER_BINARY` | `minder` | Path to the minder CLI binary | +| `TEST_USER` | `smoke-test-user` | Keycloak test user to create | +| `TEST_PASS` | `smoke-test-password` | Test user password | + +#### CI Pipeline + +Integration tests run automatically via GitHub Actions: +- **On demand**: via `workflow_dispatch` +- **Weekly**: Monday 06:00 UTC +- **On PR**: when CI infrastructure files change + +See `.github/workflows/integration-tests.yml` for details. diff --git a/Taskfile.yml b/Taskfile.yml index 7ae0092..204c6a7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -199,3 +199,75 @@ tasks: echo "Enabling subprojects for root projects..." docker exec -it postgres_container psql -h localhost -p 5432 -U postgres -d minder -c "$SET_HIERARCHICAL_SQL" + # ----------------------------------------------------------------------- + # Integration test targets — run smoke tests against a Minder Docker stack + # ----------------------------------------------------------------------- + + integration-test: + desc: Run full integration test lifecycle (setup → test → teardown) + summary: | + Starts a Minder Docker stack, bootstraps a test user, runs the smoke + tests, and tears everything down. + + Requires: + - MINDER_REPO_PATH: path to a cloned mindersec/minder repo + (default: ../minder) + - Docker / Podman + + Optional: + - MINDER_TEST_ORG: GitHub org for repo-related tests + - GH_TOKEN: GitHub token for repo operations + + Example: + MINDER_REPO_PATH=~/src/minder task integration-test + cmds: + - task: integration-setup + - defer: { task: integration-teardown } + - task: integration-run + + integration-setup: + desc: Start Minder Docker stack and bootstrap test user + vars: + MINDER_REPO_PATH: '{{.MINDER_REPO_PATH | default "../minder"}}' + cmds: + - cmd: | + echo "Starting Minder Docker stack..." + docker compose -f {{.MINDER_REPO_PATH}}/docker-compose.yaml up -d --wait + silent: false + - cmd: | + echo "Bootstrapping test user..." + ./scripts/bootstrap.sh + silent: false + env: + KEYCLOAK_URL: http://localhost:8081 + MINDER_API_URL: http://localhost:8080 + OFFLINE_TOKEN_OUTPUT_PATH: ./offline.token + CLIENT_ID: smoke-test-client + preconditions: + - sh: test -f scripts/bootstrap.sh + msg: "bootstrap.sh not found. Are you in the smoke-tests directory?" + - sh: test -f {{.MINDER_REPO_PATH}}/docker-compose.yaml + msg: "Minder docker-compose.yaml not found at {{.MINDER_REPO_PATH}}. Set MINDER_REPO_PATH." + + integration-run: + desc: Run smoke tests against a running Minder stack + cmds: + - task: test + env: + MINDER_CONFIG: '{{.ROOT_DIR}}/smoke-test-config.yaml' + MINDER_OFFLINE_TOKEN_PATH: '{{.ROOT_DIR}}/offline.token' + + integration-teardown: + desc: Tear down the Minder Docker stack + vars: + MINDER_REPO_PATH: '{{.MINDER_REPO_PATH | default "../minder"}}' + cmds: + - cmd: | + echo "Tearing down Minder Docker stack..." + docker compose -f {{.MINDER_REPO_PATH}}/docker-compose.yaml down -v --remove-orphans + silent: false + - cmd: | + echo "Cleaning up generated files..." + rm -f offline.token minder-project-id + silent: false + diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..c25d0c0 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,43 @@ +# docker-compose.test.yml +# +# Compose override that adds the smoke-test runner to the Minder Docker stack. +# This file is meant to be used alongside the main Minder docker-compose.yaml: +# +# docker compose -f /path/to/minder/docker-compose.yaml \ +# -f docker-compose.test.yml up --abort-on-container-exit +# +# The smoke-test runner waits for the Minder server to be healthy, then +# executes the Robot Framework test suite and exits. + +services: + smoke-tests: + container_name: smoke_test_runner + build: + context: . + dockerfile: Dockerfile + depends_on: + minder: + condition: service_healthy + environment: + - MINDER_CONFIG=/etc/minder/config.yaml + - MINDER_OFFLINE_TOKEN_PATH=/opt/minder-offline.token + - MINDER_TEST_ORG=${MINDER_TEST_ORG:-} + - GH_TOKEN=${GH_TOKEN:-} + - MINDER_RULETYPES_PATH=/opt/minder-ruletypes + volumes: + - ./:/robottests:z + - ./offline.token:/opt/minder-offline.token:z + - ./smoke-test-config-docker.yaml:/etc/minder/config.yaml:z + networks: + - app_net + command: > + --outputdir /robottests/results + --pythonpath /robottests + --xunit /robottests/results/xoutput.xml + --loglevel INFO + /robottests/minder-tests + +networks: + app_net: + external: true + name: minder_app_net diff --git a/minder-tests/api-datasources.robot b/minder-tests/api-datasources.robot index 7088b5b..1003f3f 100644 --- a/minder-tests/api-datasources.robot +++ b/minder-tests/api-datasources.robot @@ -4,6 +4,8 @@ Documentation Test suite for the Minder data sources REST API Resource resources/keywords.robot Library resources.datasources.DataSources +Test Tags smoke core + Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-history.robot b/minder-tests/api-history.robot index 789f79a..71dc113 100644 --- a/minder-tests/api-history.robot +++ b/minder-tests/api-history.robot @@ -2,6 +2,8 @@ Resource resources/keywords.robot Resource resources/variables.robot +Test Tags smoke github-required provider-required + Library resources.helpers Library resources.profiles.Profiles Library resources.github.GitHub diff --git a/minder-tests/api-profiles.robot b/minder-tests/api-profiles.robot index 8e5724e..e74b9ec 100644 --- a/minder-tests/api-profiles.robot +++ b/minder-tests/api-profiles.robot @@ -14,6 +14,8 @@ Library resources.profiles.Profiles Library resources.minder_restapi_lib.MinderRestApiLib Library resources.minderlib +Test Tags smoke core + Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-project.robot b/minder-tests/api-project.robot index e21567a..3b14b63 100644 --- a/minder-tests/api-project.robot +++ b/minder-tests/api-project.robot @@ -2,6 +2,8 @@ Resource resources/keywords.robot Library resources.projects.Projects +Test Tags smoke core + Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-provider.robot b/minder-tests/api-provider.robot index 37ef4b9..5e39c9d 100644 --- a/minder-tests/api-provider.robot +++ b/minder-tests/api-provider.robot @@ -2,6 +2,8 @@ Resource resources/keywords.robot Library resources.oauth_service.OAuthService +Test Tags smoke core + Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-repositories.robot b/minder-tests/api-repositories.robot index 5362bbd..a029f41 100644 --- a/minder-tests/api-repositories.robot +++ b/minder-tests/api-repositories.robot @@ -2,6 +2,8 @@ Resource resources/keywords.robot Resource resources/variables.robot +Test Tags smoke github-required provider-required + Library OperatingSystem Library BuiltIn Library RequestsLibrary diff --git a/minder-tests/api-tests.robot b/minder-tests/api-tests.robot index 4caf101..a934acb 100644 --- a/minder-tests/api-tests.robot +++ b/minder-tests/api-tests.robot @@ -11,6 +11,8 @@ Library resources.minder_restapi_lib.MinderRestApiLib Library resources.eval_results_service.EvalResultsService Library resources.rule_type_service.RuleTypeService +Test Tags smoke core + Suite Setup Load Config Test Setup Create Project And Ruletypes diff --git a/minder-tests/minder-tests.robot b/minder-tests/minder-tests.robot index 671c36f..9aa1527 100644 --- a/minder-tests/minder-tests.robot +++ b/minder-tests/minder-tests.robot @@ -1,7 +1,7 @@ *** Settings *** Documentation Test suite for some simple Minder operations -Test Tags smoke +Test Tags smoke core Resource resources/keywords.robot Resource resources/variables.robot @@ -20,7 +20,7 @@ ${GRPC_BASE_URL} None # Placeholder for the value that will be set in S *** Test Cases *** Valid login [Documentation] Test that a user can log in and get their profile - [Tags] login + [Tags] login smoke core Given I Am Logged Into Minder When I Get The User Profile @@ -28,6 +28,7 @@ Valid login Provider enrolled [Documentation] Test that a user has at least one provider + [Tags] smoke github-required provider-required Given I Am Logged Into Minder When I List My Providers @@ -35,6 +36,7 @@ Provider enrolled Project created [Documentation] Test that a user has at least one project + [Tags] smoke core Given I Am Logged Into Minder When I List My Projects diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..887a839 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,201 @@ +#!/bin/bash +# bootstrap.sh — Programmatically create a test user in Keycloak and generate +# an offline token for use by the smoke-test suite. +# +# This script eliminates the need for a human to manually authenticate via +# browser-based GitHub OAuth, enabling fully automated integration testing. +# +# Prerequisites: +# - Keycloak is running and healthy +# - Minder server is running (or will be shortly) +# - curl, jq, and minder CLI are available on PATH +# +# Usage: +# ./scripts/bootstrap.sh +# +# Environment variables (all have sensible defaults for run-docker): +# KEYCLOAK_URL — Keycloak base URL (default: http://localhost:8081) +# KC_REALM — Keycloak realm (default: stacklok) +# KC_ADMIN_USER — Keycloak admin username (default: admin) +# KC_ADMIN_PASS — Keycloak admin password (default: admin) +# TEST_USER — Test user to create (default: smoke-test-user) +# TEST_PASS — Test user password (default: smoke-test-password) +# CLIENT_ID — OIDC client ID (default: smoke-test-client) +# MINDER_API_URL — Minder HTTP API URL (default: http://localhost:8080) +# OFFLINE_TOKEN_OUTPUT_PATH — Where to write token (default: ./offline.token) +# MINDER_BINARY — Path to minder binary (default: minder) + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8081}" +KC_REALM="${KC_REALM:-stacklok}" +KC_ADMIN_USER="${KC_ADMIN_USER:-admin}" +KC_ADMIN_PASS="${KC_ADMIN_PASS:-admin}" +TEST_USER="${TEST_USER:-smoke-test-user}" +TEST_PASS="${TEST_PASS:-smoke-test-password}" +CLIENT_ID="${CLIENT_ID:-smoke-test-client}" +MINDER_API_URL="${MINDER_API_URL:-http://localhost:8080}" +OFFLINE_TOKEN_OUTPUT_PATH="${OFFLINE_TOKEN_OUTPUT_PATH:-./offline.token}" +MINDER_BINARY="${MINDER_BINARY:-minder}" + +MAX_RETRIES=30 +RETRY_INTERVAL=5 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +log() { echo "==> $*"; } +warn() { echo "==> [WARN] $*" >&2; } +fatal() { echo "==> [FATAL] $*" >&2; exit 1; } + +wait_for_url() { + local url="$1" + local label="$2" + local retries=0 + + log "Waiting for ${label} at ${url}..." + until curl -sf "${url}" > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ "${retries}" -ge "${MAX_RETRIES}" ]; then + fatal "${label} did not become ready after ${MAX_RETRIES} attempts" + fi + sleep "${RETRY_INTERVAL}" + done + log "${label} is ready." +} + +# --------------------------------------------------------------------------- +# Step 1: Wait for Keycloak +# --------------------------------------------------------------------------- + +wait_for_url "${KEYCLOAK_URL}/health/ready" "Keycloak" + +# --------------------------------------------------------------------------- +# Step 2: Obtain Keycloak admin access token +# --------------------------------------------------------------------------- + +log "Obtaining Keycloak admin access token..." +ADMIN_TOKEN_RESPONSE=$(curl -sf -X POST \ + "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ + -d "grant_type=password" \ + -d "username=${KC_ADMIN_USER}" \ + -d "password=${KC_ADMIN_PASS}" \ + -d "client_id=admin-cli") \ + || fatal "Failed to obtain admin token from Keycloak" + +ADMIN_TOKEN=$(echo "${ADMIN_TOKEN_RESPONSE}" | jq -r '.access_token') +if [ -z "${ADMIN_TOKEN}" ] || [ "${ADMIN_TOKEN}" = "null" ]; then + fatal "Admin access token is empty or null" +fi +log "Admin token obtained successfully." + +# --------------------------------------------------------------------------- +# Step 3: Create test user in Keycloak +# --------------------------------------------------------------------------- + +log "Creating test user '${TEST_USER}' in realm '${KC_REALM}'..." +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "${KEYCLOAK_URL}/admin/realms/${KC_REALM}/users" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"${TEST_USER}\", + \"enabled\": true, + \"emailVerified\": true, + \"email\": \"${TEST_USER}@test.local\", + \"firstName\": \"Smoke\", + \"lastName\": \"Test\", + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"${TEST_PASS}\", + \"temporary\": false + }] + }") + +case "${HTTP_STATUS}" in + 201) log "User '${TEST_USER}' created successfully." ;; + 409) log "User '${TEST_USER}' already exists — skipping creation." ;; + *) fatal "Unexpected status ${HTTP_STATUS} creating user. Check Keycloak logs." ;; +esac + +# --------------------------------------------------------------------------- +# Step 4: Request offline token via Resource Owner Password Credentials +# --------------------------------------------------------------------------- + +log "Requesting offline token for '${TEST_USER}' via ROPC grant..." +TOKEN_RESPONSE=$(curl -sf -X POST \ + "${KEYCLOAK_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \ + -d "grant_type=password" \ + -d "client_id=${CLIENT_ID}" \ + -d "username=${TEST_USER}" \ + -d "password=${TEST_PASS}" \ + -d "scope=openid offline_access") \ + || fatal "Failed to obtain offline token. Is the '${CLIENT_ID}' Keycloak client configured with Direct Access Grants (ROPC) enabled and the 'offline_access' scope? Create a 'smoke-test-client' in the Keycloak realm config with directAccessGrantsEnabled=true." + +OFFLINE_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.refresh_token') +if [ -z "${OFFLINE_TOKEN}" ] || [ "${OFFLINE_TOKEN}" = "null" ]; then + echo "Token response:" >&2 + echo "${TOKEN_RESPONSE}" | jq '.' >&2 + fatal "Offline token (refresh_token) is empty or null. The '${CLIENT_ID}' client may not have offline_access scope or ROPC enabled." +fi + +echo "${OFFLINE_TOKEN}" > "${OFFLINE_TOKEN_OUTPUT_PATH}" +log "Offline token saved to ${OFFLINE_TOKEN_OUTPUT_PATH}" + +# --------------------------------------------------------------------------- +# Step 5: Wait for Minder server +# --------------------------------------------------------------------------- + +wait_for_url "${MINDER_API_URL}/api/v1/health" "Minder server" + +# --------------------------------------------------------------------------- +# Step 6: Bootstrap user in Minder (first auth triggers auto-enrollment) +# --------------------------------------------------------------------------- + +log "Authenticating test user with Minder (triggers auto-enrollment)..." +${MINDER_BINARY} auth offline-token use --file "${OFFLINE_TOKEN_OUTPUT_PATH}" \ + || fatal "Failed to authenticate with Minder using offline token" + +# --------------------------------------------------------------------------- +# Step 7: Extract root project ID +# --------------------------------------------------------------------------- + +log "Retrieving root project ID..." +PROJECT_LIST=$(${MINDER_BINARY} project list -o json 2>/dev/null) \ + || fatal "Failed to list Minder projects" + +PROJECT_ID=$(echo "${PROJECT_LIST}" | jq -r '.projects[0].projectId // empty') +if [ -z "${PROJECT_ID}" ]; then + fatal "No projects found. Auto-enrollment may have failed." +fi + +log "Root project ID: ${PROJECT_ID}" + +# Export for downstream use (e.g., GitHub Actions, Taskfile) +export MINDER_PROJECT="${PROJECT_ID}" + +# If running in GitHub Actions, persist to $GITHUB_ENV +if [ -n "${GITHUB_ENV:-}" ]; then + echo "MINDER_PROJECT=${PROJECT_ID}" >> "${GITHUB_ENV}" + log "MINDER_PROJECT exported to GITHUB_ENV" +fi + +# Also write to a file for non-GHA consumers +echo "${PROJECT_ID}" > ./minder-project-id +log "Project ID written to ./minder-project-id" + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- + +log "============================================" +log " Bootstrap complete!" +log " User: ${TEST_USER}" +log " Token: ${OFFLINE_TOKEN_OUTPUT_PATH}" +log " Project: ${PROJECT_ID}" +log "============================================" diff --git a/smoke-test-config-docker.yaml b/smoke-test-config-docker.yaml new file mode 100644 index 0000000..a21e381 --- /dev/null +++ b/smoke-test-config-docker.yaml @@ -0,0 +1,22 @@ +# smoke-test-config-docker.yaml +# +# Minder CLI configuration for when the test runner runs INSIDE the +# Docker Compose network (via docker-compose.test.yml). +# Uses Docker service names instead of localhost. + +http_server: + host: minder + port: 8080 + +grpc_server: + host: minder + port: 8090 + insecure: true + +identity: + cli: + # The issuer_url uses 'keycloak' (Docker service name) because the + # token exchange happens inside the Docker network. + issuer_url: http://keycloak:8080 + realm: stacklok + client_id: minder-cli diff --git a/smoke-test-config.yaml b/smoke-test-config.yaml new file mode 100644 index 0000000..c6ca212 --- /dev/null +++ b/smoke-test-config.yaml @@ -0,0 +1,25 @@ +# smoke-test-config.yaml +# +# Minder CLI configuration for running smoke tests against a local +# Docker Compose Minder stack. When the test runner is INSIDE the +# Docker network (via docker-compose.test.yml), use the service names +# (minder, keycloak). When running from the HOST, use localhost with +# the mapped ports. +# +# This file is the "inside Docker" variant. For host-side use, see +# the README instructions to override with localhost values. + +http_server: + host: localhost + port: 8080 + +grpc_server: + host: localhost + port: 8090 + insecure: true + +identity: + cli: + issuer_url: http://localhost:8081 + realm: stacklok + client_id: minder-cli From 226de5578d23d6b7a85e08e60a74247a2c6a51e8 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Tue, 9 Jun 2026 12:48:31 +0530 Subject: [PATCH 2/6] fix: address reviewer feedback on Phase 1 CI workflow and task targets Address four issues raised in code review: ## 1. Security: remove freeform minder_ref input (workflow_dispatch) The previous workflow allowed specifying any minder branch/tag/SHA via workflow_dispatch input. An attacker could point this at a fork commit containing malicious Makefile or docker-compose code, which would then execute in the GitHub Actions context with access to repo secrets. Fix: remove the minder_ref input entirely. We always test against mindersec/minder@main using pre-built published images. The workflow no longer checks out the minder repo or runs its Makefile. ## 2. Performance: use pre-built minder images instead of building from source The previous workflow ran make bootstrap + make build + ko resolve to build the minder server image from Go source. This added ~10 minutes of setup (Go toolchain, ko, protoc, sqlc, etc.) before any test ran. Fix: download the minder CLI binary from the latest GitHub release and pull the minder server Docker image from the registry using docker compose pull. Eliminates the Go/ko/bootstrap setup steps entirely. ## 3. Redundancy: remove explicit health check step The previous workflow had a dedicated 'Wait for Minder stack health' step that polled /api/v1/health and /health/ready. This was redundant because: - docker compose up --wait already blocks until healthchecks pass - bootstrap.sh has its own wait_for_url loops as a safety net Fix: remove the step. Rely on docker compose up --wait and bootstrap.sh. ## 4. Architecture: run tests directly on the runner (no container image) The previous workflow ran task build (build a Docker image with Robot + Python) then task test (run tests inside that container). Building a container just to run Python tests in CI adds complexity for no benefit since the runner already has Python. Fix: install requirements.txt via pip directly on the runner and invoke robot as a host command. The container-based task test is kept for local dev isolation but is not used in CI. Also adds task integration-run-core as a convenience shortcut and clarifies the distinction between task test (container, local dev) and task integration-run (host robot, CI / pre-existing stack) in both the Taskfile and README. --- .github/workflows/integration-tests.yml | 163 +++++++++++------------- README.md | 44 ++++--- Taskfile.yml | 50 ++++++-- minder-tests/api-datasources.robot | 2 - minder-tests/api-profiles.robot | 2 - minder-tests/api-project.robot | 2 - minder-tests/api-provider.robot | 2 - minder-tests/api-tests.robot | 2 - minder-tests/minder-tests.robot | 4 +- scripts/bootstrap.sh | 6 +- 10 files changed, 144 insertions(+), 133 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f27a8e0..4d87a62 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -2,39 +2,35 @@ name: Integration Tests on: - # Manual trigger for on-demand runs + # Manual trigger for on-demand runs. + # NOTE: minder is always tested at mindersec/minder@main to prevent + # impersonation attacks where a fork's malicious Makefile/compose + # code could run with access to this repo's secrets. workflow_dispatch: inputs: - minder_ref: - description: 'Minder branch/tag to test against' - required: false - default: 'main' test_tags: - description: 'Robot Framework tag filter (e.g. "smoke", "core")' + description: 'Robot Framework tag filter (e.g. "core", "smoke")' required: false default: '' # Weekly scheduled run schedule: - - cron: '0 6 * * 1' # Every Monday at 6:00 AM UTC + - cron: "0 6 * * 1" # Every Monday at 6:00 AM UTC # Run on PRs to validate test infrastructure changes pull_request: paths: - - 'scripts/**' - - 'docker-compose.test.yml' - - 'smoke-test-config*.yaml' - - '.github/workflows/integration-tests.yml' - - 'Dockerfile' - - 'requirements.txt' + - "scripts/**" + - "minder-tests/**" + - "resources/**" + - "smoke-test-config*.yaml" + - ".github/workflows/integration-tests.yml" + - "requirements.txt" concurrency: group: integration-tests-${{ github.ref }} cancel-in-progress: true -env: - MINDER_REF: ${{ github.event.inputs.minder_ref || 'main' }} - jobs: integration-test: name: Run Integration Tests @@ -43,112 +39,99 @@ jobs: permissions: contents: read - id-token: write # For potential OIDC use in the future steps: # ------------------------------------------------------------------ - # Checkout + # Checkout smoke-tests only. + # We always test against mindersec/minder@main using pre-built + # images — we do NOT checkout the minder repo or run its Makefile. # ------------------------------------------------------------------ - name: Checkout smoke-tests uses: actions/checkout@v4 - - name: Checkout minder - uses: actions/checkout@v4 - with: - repository: mindersec/minder - ref: ${{ env.MINDER_REF }} - path: minder - # ------------------------------------------------------------------ - # Dependencies + # Install test dependencies directly on the runner. + # No container image needed for CI — the runner already has Python. # ------------------------------------------------------------------ - - name: Setup Go - uses: actions/setup-go@v5 + - name: Set up Python + uses: actions/setup-python@v5 with: - go-version: '1.23' - cache-dependency-path: minder/go.sum + python-version: '3.11' + cache: pip - - name: Install ko - uses: ko-build/setup-ko@v0.6 - - - name: Install yq - run: | - sudo wget -qO /usr/local/bin/yq \ - https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq - - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x + - name: Install Robot Framework and dependencies + run: pip install -r requirements.txt # ------------------------------------------------------------------ - # Build Minder + # Download the minder CLI binary from the latest release. + # This avoids building minder from source (saves ~10 minutes). # ------------------------------------------------------------------ - - name: Bootstrap Minder - working-directory: minder - run: make bootstrap - - - name: Build Minder - working-directory: minder - run: make build + - name: Install minder CLI + run: | + MINDER_VERSION=$(curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ + | jq -r '.tag_name') + echo "Downloading minder CLI ${MINDER_VERSION}..." + curl -sL \ + "https://github.com/mindersec/minder/releases/download/${MINDER_VERSION}/minder_Linux_x86_64.tar.gz" \ + | tar -xz -C /usr/local/bin minder + chmod +x /usr/local/bin/minder + minder version # ------------------------------------------------------------------ - # Start Minder stack + # Pull Minder compose file and start the stack using pre-built images. + # docker compose up --wait blocks until all healthchecks pass — + # no extra polling step needed. # ------------------------------------------------------------------ - - name: Start Minder Docker stack - working-directory: minder - run: make run-docker + - name: Download Minder docker-compose + run: | + curl -sLo docker-compose.minder.yaml \ + https://raw.githubusercontent.com/mindersec/minder/main/docker-compose.yaml - - name: Wait for Minder stack health + - name: Start Minder Docker stack run: | - echo "Waiting for Minder server to be healthy..." - timeout 180 bash -c ' - until curl -sf http://localhost:8080/api/v1/health > /dev/null 2>&1; do - echo " ...waiting" - sleep 5 - done - ' - echo "Minder server is healthy!" - - echo "Waiting for Keycloak..." - timeout 120 bash -c ' - until curl -sf http://localhost:8081/health/ready > /dev/null 2>&1; do - echo " ...waiting" - sleep 5 - done - ' - echo "Keycloak is healthy!" - - # ------------------------------------------------------------------ - # Bootstrap test user + # Pull images first so startup is fast and errors are clear + docker compose -f docker-compose.minder.yaml pull + # --wait blocks until all service healthchecks pass + docker compose -f docker-compose.minder.yaml up -d --wait + env: + # bootstrap.sh waits independently, but --wait on compose + # means we already know both minder + keycloak are healthy here + KO_DOCKER_REPO: "" # not needed — using pre-pulled images + + # ------------------------------------------------------------------ + # Bootstrap: create test user in Keycloak, obtain offline token, + # trigger Minder user auto-enrollment, extract project ID. + # bootstrap.sh has its own wait_for_url loops as a safety net. # ------------------------------------------------------------------ - name: Bootstrap test user and project run: ./scripts/bootstrap.sh env: KEYCLOAK_URL: http://localhost:8081 MINDER_API_URL: http://localhost:8080 - MINDER_BINARY: ${{ github.workspace }}/minder/bin/minder + MINDER_BINARY: /usr/local/bin/minder OFFLINE_TOKEN_OUTPUT_PATH: ${{ github.workspace }}/offline.token CLIENT_ID: smoke-test-client # ------------------------------------------------------------------ - # Run tests + # Run Robot Framework tests directly on the runner (no container). # ------------------------------------------------------------------ - - name: Build smoke test image - run: task build - - name: Run smoke tests run: | EXTRA_ARGS="" if [ -n "${{ github.event.inputs.test_tags }}" ]; then EXTRA_ARGS="-i ${{ github.event.inputs.test_tags }}" fi - task test -- ${EXTRA_ARGS} + robot \ + --outputdir results \ + --pythonpath . \ + --xunit results/xoutput.xml \ + --loglevel INFO \ + ${EXTRA_ARGS} \ + minder-tests/ env: MINDER_CONFIG: ${{ github.workspace }}/smoke-test-config.yaml MINDER_OFFLINE_TOKEN_PATH: ${{ github.workspace }}/offline.token - MINDER_BINARY_PATH: ${{ github.workspace }}/minder/bin/minder + MINDER_PROJECT: ${{ env.MINDER_PROJECT }} MINDER_TEST_ORG: ${{ secrets.MINDER_TEST_ORG }} GH_TOKEN: ${{ secrets.GH_TOKEN }} @@ -160,15 +143,14 @@ jobs: uses: actions/upload-artifact@v4 with: name: robot-test-results - path: | - results/ + path: results/ retention-days: 30 - name: Publish test report if: always() uses: dorny/test-reporter@v1 with: - name: 'Integration Test Results' + name: "Integration Test Results" path: results/xoutput.xml reporter: java-junit fail-on-error: true @@ -178,15 +160,12 @@ jobs: # ------------------------------------------------------------------ - name: Show Minder logs on failure if: failure() - working-directory: minder - run: docker compose logs minder --tail=100 + run: docker compose -f docker-compose.minder.yaml logs minder --tail=100 - name: Show Keycloak logs on failure if: failure() - working-directory: minder - run: docker compose logs keycloak --tail=50 + run: docker compose -f docker-compose.minder.yaml logs keycloak --tail=50 - name: Teardown Minder stack if: always() - working-directory: minder - run: docker compose down -v --remove-orphans + run: docker compose -f docker-compose.minder.yaml down -v --remove-orphans diff --git a/README.md b/README.md index a1904ec..b0dd7c9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ task test Since the tests run in a container, they need a `minder` Linux binary in the path. If you're runninng on a non-Linux machine, you need to provide one with an environment variable: + ```bash MINDER_BINARY_PATH=/path/to/minder task test ``` @@ -30,6 +31,7 @@ tests that create, modify, or delete repositories and pull requests. It is possible to specify the org specifying `MINDER_TEST_ORG=` when running `task test`, so the previous example becomes + ```bash MINDER_BINARY_PATH=/path/to/minder MINDER_TEST_ORG=my-org-name task test ``` @@ -42,6 +44,7 @@ section for instructions on how to create repos in a test org. The `task test` command will authenticate using an offline token, by default using the `offline.token` file in the current directory. If you want to test against a different environment, you need to provide a configuration file that contains the endpoints and credentials for the environments you want to test against. For example, to run the tests against the staging environment, you can use the following command: + ```bash MINDER_CONFIG=$(pwd)/staging-config.yaml MINDER_OFFLINE_TOKEN_PATH=$(pwd)/staging-offline.token task test ``` @@ -71,6 +74,7 @@ Confusingly, the `issuer_url` needs to be `localhost` as that corresponds to the ### Using ruletypes from a local repository If you want to run the tests against a local Minder instance with ruletypes from a local repository, you can pass the path to the ruletypes directory as an environment variable. + ```bash MINDER_RULETYPES_PATH=$(pwd)/path/to/ruletypes task test ``` @@ -125,7 +129,6 @@ Valid user login Then the user is logged in ``` - ### Writing custom libraries Custom libraries are written in the `resources` directory. Each library should have its own @@ -169,22 +172,22 @@ started via `docker compose` (from the main - A cloned copy of the `mindersec/minder` repository - `curl`, `jq` on the host - The `minder` CLI binary on PATH (or set `MINDER_BINARY`) -- A `smoke-test-client` OIDC client in the Keycloak realm with **Direct Access Grants (ROPC) enabled**. - This client must be added to the Keycloak realm JSON in the `mindersec/minder` repo - (`deploy/k8s/keycloak/` or the `run-docker` realm import file) before the bootstrap script - will succeed. See the bootstrap script header for details. #### Quick start ```bash -# 1. Clone minder next to the smoke-tests repo (or set MINDER_REPO_PATH) +# 1. Install Robot Framework on your host (one-time) +pip install -r requirements.txt + +# 2. Clone minder next to the smoke-tests repo (or set MINDER_REPO_PATH) git clone git@github.com:mindersec/minder.git ../minder -# 2. Run the full integration test lifecycle +# 3. Run the full integration test lifecycle task integration-test ``` This will: + 1. Start the Minder Docker stack (`docker compose up`) 2. Run `scripts/bootstrap.sh` to create a test user in Keycloak and generate an offline token (no browser interaction needed) @@ -194,21 +197,28 @@ This will: #### Running tests manually (step by step) ```bash -# Start Minder stack +# Install Robot Framework (one-time) +pip install -r requirements.txt + +# Start Minder stack and bootstrap test user task integration-setup -# Run tests (can be repeated without re-setup) +# Run all tests against the running stack (no container needed) task integration-run -# Optionally, run only core tests (no GitHub provider needed): -MINDER_CONFIG=$(pwd)/smoke-test-config.yaml \ - MINDER_OFFLINE_TOKEN_PATH=$(pwd)/offline.token \ - task test -- -i core +# OR run only core tests (no GitHub provider needed): +task integration-run-core +# which is equivalent to: +task integration-run TEST_TAGS=core # Tear down task integration-teardown ``` +> **Note on two execution modes:** +> - `task test` — runs tests **inside a Docker container** (good for local dev isolation, matches the original test runner) +> - `task integration-run` — runs tests **directly on the host** using the installed `robot` command (used by CI and for testing against a running stack with manual setup) + #### Test tags Tests are tagged by their infrastructure requirements: @@ -216,17 +226,18 @@ Tests are tagged by their infrastructure requirements: | Tag | Meaning | |-----|---------| | `smoke` | All smoke tests (default) | -| `core` | Tests that only need Minder API + auth token (no GitHub) | -| `login` | Authentication / whoami tests | +| `core` | Tests that only need Minder API (no GitHub) | | `github-required` | Tests that need a live GitHub org and token | -| `provider-required` | Tests that need an enrolled GitHub App provider | +| `provider-required` | Tests that need an enrolled GitHub provider | To run only core tests: + ```bash task test -- -i core ``` To exclude GitHub-dependent tests: + ```bash task test -- -e github-required ``` @@ -245,6 +256,7 @@ task test -- -e github-required #### CI Pipeline Integration tests run automatically via GitHub Actions: + - **On demand**: via `workflow_dispatch` - **Weekly**: Monday 06:00 UTC - **On PR**: when CI infrastructure files change diff --git a/Taskfile.yml b/Taskfile.yml index 204c6a7..9ac0bbc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,8 +1,8 @@ --- -version: '3' +version: "3" dotenv: - - '.env' + - ".env" env: IMAGE: localhost/smoke-test-experiments @@ -26,7 +26,7 @@ tasks: The minder configuration file is only mounted if it is provided in the environment. Available variables: - + - MINDER_BINARY_PATH: The path to the minder binary. If not provided, the task will try to find the minder binary in the system. - MINDER_CONFIG: The path to the minder configuration file. If not provided, @@ -74,7 +74,7 @@ tasks: CTESTDIR: /robottests vars: _MINDER_BINARY_PATH: - sh: '{{if .MINDER_BINARY_PATH}} echo {{.MINDER_BINARY_PATH}} {{else}} which minder {{end}}' + sh: "{{if .MINDER_BINARY_PATH}} echo {{.MINDER_BINARY_PATH}} {{else}} which minder {{end}}" _CONTAINER_OPTS: '{{._GLOBAL_CONTAINER_OPTS}} {{if ._CONTAINER_CMD | contains "podman"}} {{._PODMAN_OPTS}} {{end}}' preconditions: - sh: test -f {{._MINDER_BINARY_PATH}} @@ -207,16 +207,21 @@ tasks: desc: Run full integration test lifecycle (setup → test → teardown) summary: | Starts a Minder Docker stack, bootstraps a test user, runs the smoke - tests, and tears everything down. + tests directly via robot (no container), and tears everything down. Requires: - MINDER_REPO_PATH: path to a cloned mindersec/minder repo (default: ../minder) - Docker / Podman + - Robot Framework: pip install -r requirements.txt Optional: - MINDER_TEST_ORG: GitHub org for repo-related tests - GH_TOKEN: GitHub token for repo operations + - TEST_TAGS: Robot tag filter passed to integration-run + + To run only core tests (no GitHub needed): + task integration-test TEST_TAGS=core Example: MINDER_REPO_PATH=~/src/minder task integration-test @@ -242,20 +247,48 @@ tasks: KEYCLOAK_URL: http://localhost:8081 MINDER_API_URL: http://localhost:8080 OFFLINE_TOKEN_OUTPUT_PATH: ./offline.token - CLIENT_ID: smoke-test-client preconditions: - sh: test -f scripts/bootstrap.sh msg: "bootstrap.sh not found. Are you in the smoke-tests directory?" - sh: test -f {{.MINDER_REPO_PATH}}/docker-compose.yaml msg: "Minder docker-compose.yaml not found at {{.MINDER_REPO_PATH}}. Set MINDER_REPO_PATH." + # integration-run: run tests against a *already running* Minder stack. + # This uses robot directly (no container) — the host Python must have + # requirements.txt installed (e.g. `pip install -r requirements.txt`). + # + # NOTE: This is distinct from `task test`, which builds + runs a + # container image. Use `task test` for local dev isolation; use + # `task integration-run` when running against a pre-existing stack + # (e.g. in CI, or after `task integration-setup`). integration-run: - desc: Run smoke tests against a running Minder stack + desc: Run smoke tests against a running Minder stack (no container) + vars: + _TAG_FILTER: '{{if .TEST_TAGS}}-i {{.TEST_TAGS}}{{end}}' cmds: - - task: test + - cmd: | + robot \ + --outputdir {{.ROOT_DIR}}/results \ + --pythonpath {{.ROOT_DIR}} \ + --xunit {{.ROOT_DIR}}/results/xoutput.xml \ + --loglevel INFO \ + {{._TAG_FILTER}} \ + {{.ROOT_DIR}}/minder-tests env: MINDER_CONFIG: '{{.ROOT_DIR}}/smoke-test-config.yaml' MINDER_OFFLINE_TOKEN_PATH: '{{.ROOT_DIR}}/offline.token' + preconditions: + - sh: test -f {{.ROOT_DIR}}/offline.token + msg: "offline.token not found. Run task integration-setup first." + - sh: python3 -c "import robot" 2>/dev/null + msg: "Robot Framework not installed. Run: pip install -r requirements.txt" + + integration-run-core: + desc: Run only core tests (no GitHub provider needed) + cmds: + - task: integration-run + vars: + TEST_TAGS: core integration-teardown: desc: Tear down the Minder Docker stack @@ -270,4 +303,3 @@ tasks: echo "Cleaning up generated files..." rm -f offline.token minder-project-id silent: false - diff --git a/minder-tests/api-datasources.robot b/minder-tests/api-datasources.robot index 1003f3f..7088b5b 100644 --- a/minder-tests/api-datasources.robot +++ b/minder-tests/api-datasources.robot @@ -4,8 +4,6 @@ Documentation Test suite for the Minder data sources REST API Resource resources/keywords.robot Library resources.datasources.DataSources -Test Tags smoke core - Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-profiles.robot b/minder-tests/api-profiles.robot index e74b9ec..8e5724e 100644 --- a/minder-tests/api-profiles.robot +++ b/minder-tests/api-profiles.robot @@ -14,8 +14,6 @@ Library resources.profiles.Profiles Library resources.minder_restapi_lib.MinderRestApiLib Library resources.minderlib -Test Tags smoke core - Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-project.robot b/minder-tests/api-project.robot index 3b14b63..e21567a 100644 --- a/minder-tests/api-project.robot +++ b/minder-tests/api-project.robot @@ -2,8 +2,6 @@ Resource resources/keywords.robot Library resources.projects.Projects -Test Tags smoke core - Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-provider.robot b/minder-tests/api-provider.robot index 5e39c9d..37ef4b9 100644 --- a/minder-tests/api-provider.robot +++ b/minder-tests/api-provider.robot @@ -2,8 +2,6 @@ Resource resources/keywords.robot Library resources.oauth_service.OAuthService -Test Tags smoke core - Suite Setup Load Config Test Setup Default Setup diff --git a/minder-tests/api-tests.robot b/minder-tests/api-tests.robot index a934acb..4caf101 100644 --- a/minder-tests/api-tests.robot +++ b/minder-tests/api-tests.robot @@ -11,8 +11,6 @@ Library resources.minder_restapi_lib.MinderRestApiLib Library resources.eval_results_service.EvalResultsService Library resources.rule_type_service.RuleTypeService -Test Tags smoke core - Suite Setup Load Config Test Setup Create Project And Ruletypes diff --git a/minder-tests/minder-tests.robot b/minder-tests/minder-tests.robot index 9aa1527..644aa36 100644 --- a/minder-tests/minder-tests.robot +++ b/minder-tests/minder-tests.robot @@ -20,7 +20,7 @@ ${GRPC_BASE_URL} None # Placeholder for the value that will be set in S *** Test Cases *** Valid login [Documentation] Test that a user can log in and get their profile - [Tags] login smoke core + [Tags] login Given I Am Logged Into Minder When I Get The User Profile @@ -28,7 +28,6 @@ Valid login Provider enrolled [Documentation] Test that a user has at least one provider - [Tags] smoke github-required provider-required Given I Am Logged Into Minder When I List My Providers @@ -36,7 +35,6 @@ Provider enrolled Project created [Documentation] Test that a user has at least one project - [Tags] smoke core Given I Am Logged Into Minder When I List My Projects diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 887a839..8e3530f 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -20,7 +20,7 @@ # KC_ADMIN_PASS — Keycloak admin password (default: admin) # TEST_USER — Test user to create (default: smoke-test-user) # TEST_PASS — Test user password (default: smoke-test-password) -# CLIENT_ID — OIDC client ID (default: smoke-test-client) +# CLIENT_ID — OIDC client ID (default: minder-cli) # MINDER_API_URL — Minder HTTP API URL (default: http://localhost:8080) # OFFLINE_TOKEN_OUTPUT_PATH — Where to write token (default: ./offline.token) # MINDER_BINARY — Path to minder binary (default: minder) @@ -37,7 +37,7 @@ KC_ADMIN_USER="${KC_ADMIN_USER:-admin}" KC_ADMIN_PASS="${KC_ADMIN_PASS:-admin}" TEST_USER="${TEST_USER:-smoke-test-user}" TEST_PASS="${TEST_PASS:-smoke-test-password}" -CLIENT_ID="${CLIENT_ID:-smoke-test-client}" +CLIENT_ID="${CLIENT_ID:-minder-cli}" MINDER_API_URL="${MINDER_API_URL:-http://localhost:8080}" OFFLINE_TOKEN_OUTPUT_PATH="${OFFLINE_TOKEN_OUTPUT_PATH:-./offline.token}" MINDER_BINARY="${MINDER_BINARY:-minder}" @@ -135,7 +135,7 @@ TOKEN_RESPONSE=$(curl -sf -X POST \ -d "username=${TEST_USER}" \ -d "password=${TEST_PASS}" \ -d "scope=openid offline_access") \ - || fatal "Failed to obtain offline token. Is the '${CLIENT_ID}' Keycloak client configured with Direct Access Grants (ROPC) enabled and the 'offline_access' scope? Create a 'smoke-test-client' in the Keycloak realm config with directAccessGrantsEnabled=true." + || fatal "Failed to obtain offline token. Is the '${CLIENT_ID}' client configured for Direct Access Grants (ROPC)?" OFFLINE_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.refresh_token') if [ -z "${OFFLINE_TOKEN}" ] || [ "${OFFLINE_TOKEN}" = "null" ]; then From e7d064cf6e086e796364cfc009a474dc527efdc0 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Tue, 9 Jun 2026 12:56:46 +0530 Subject: [PATCH 3/6] fix(ci): patch compose to use pre-built images and fix config generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CI failures addressed: 1. The minder docker-compose.yaml uses 'build: context: .' for the minder server and migrate services. docker compose pull skips build- directive services, and docker compose up tries to build from local source — which fails in CI because there is no local minder checkout. Fix: fetch the compose file from mindersec/minder@main and use yq to replace build directives with 'ghcr.io/mindersec/minder:latest'. 2. The minder server requires SSH keys and config files that 'make bootstrap' normally generates from local source. Without the minder repo checked out these files are missing and docker compose up fails. Fix: generate minimal SSH key set and fetch config templates directly from mindersec/minder@main. 3. MINDER_PROJECT was referenced as '${{ env.MINDER_PROJECT }}' in the step env block. GitHub Actions evaluates step env values at parse time, not after previous steps run. bootstrap.sh writes MINDER_PROJECT to $GITHUB_ENV, making it available as a plain shell variable in subsequent steps — no explicit reference needed in the YAML. Signed-off-by: jaydeep869 --- .github/workflows/integration-tests.yml | 85 ++++++++++++++++++------- 1 file changed, 61 insertions(+), 24 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4d87a62..6790b33 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -3,7 +3,7 @@ name: Integration Tests on: # Manual trigger for on-demand runs. - # NOTE: minder is always tested at mindersec/minder@main to prevent + # NOTE: we always test mindersec/minder@main to prevent # impersonation attacks where a fork's malicious Makefile/compose # code could run with access to this repo's secrets. workflow_dispatch: @@ -42,16 +42,16 @@ jobs: steps: # ------------------------------------------------------------------ - # Checkout smoke-tests only. - # We always test against mindersec/minder@main using pre-built - # images — we do NOT checkout the minder repo or run its Makefile. + # Checkout smoke-tests. + # We test against mindersec/minder@main using pre-built images — + # we do NOT run any Makefile from the minder repo. # ------------------------------------------------------------------ - name: Checkout smoke-tests uses: actions/checkout@v4 # ------------------------------------------------------------------ # Install test dependencies directly on the runner. - # No container image needed for CI — the runner already has Python. + # No container image needed — the runner already has Python. # ------------------------------------------------------------------ - name: Set up Python uses: actions/setup-python@v5 @@ -63,8 +63,8 @@ jobs: run: pip install -r requirements.txt # ------------------------------------------------------------------ - # Download the minder CLI binary from the latest release. - # This avoids building minder from source (saves ~10 minutes). + # Install minder CLI from the latest published GitHub release. + # Avoids building from source (~10 min saved). # ------------------------------------------------------------------ - name: Install minder CLI run: | @@ -74,34 +74,67 @@ jobs: curl -sL \ "https://github.com/mindersec/minder/releases/download/${MINDER_VERSION}/minder_Linux_x86_64.tar.gz" \ | tar -xz -C /usr/local/bin minder - chmod +x /usr/local/bin/minder minder version # ------------------------------------------------------------------ - # Pull Minder compose file and start the stack using pre-built images. - # docker compose up --wait blocks until all healthchecks pass — - # no extra polling step needed. + # Fetch the minder docker-compose.yaml from main and patch it to + # use pre-built ghcr.io images instead of local build directives. + # The minder compose file has `build: context: .` for the server + # and migrate services — we replace these with the published image. # ------------------------------------------------------------------ - - name: Download Minder docker-compose + - name: Fetch and patch Minder docker-compose run: | curl -sLo docker-compose.minder.yaml \ https://raw.githubusercontent.com/mindersec/minder/main/docker-compose.yaml + # Install yq to patch the compose file + sudo wget -qO /usr/local/bin/yq \ + https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + # Replace build directives with ghcr.io pre-built images + yq e ' + del(.services.minder.build) | + .services.minder.image = "ghcr.io/mindersec/minder:latest" | + del(.services.migrate.build) | + .services.migrate.image = "ghcr.io/mindersec/minder:latest" + ' -i docker-compose.minder.yaml + + # ------------------------------------------------------------------ + # minder needs a few config files and SSH keys that make bootstrap + # normally generates. Recreate the minimal set here. + # ------------------------------------------------------------------ + - name: Generate minder runtime config + run: | + # Fetch config templates from minder main + curl -sLo server-config.yaml \ + https://raw.githubusercontent.com/mindersec/minder/main/config/server-config.yaml.example + curl -sLo flags-config.yaml \ + https://raw.githubusercontent.com/mindersec/minder/main/flags-config.yaml || echo "{}" > flags-config.yaml + + # Generate the SSH keys minder expects + mkdir -p .ssh .secrets + openssl genrsa -out .ssh/access_token_rsa 2048 2>/dev/null + openssl rsa -in .ssh/access_token_rsa -pubout -out .ssh/access_token_rsa.pub 2>/dev/null + openssl genrsa -out .ssh/refresh_token_rsa 2048 2>/dev/null + openssl rsa -in .ssh/refresh_token_rsa -pubout -out .ssh/refresh_token_rsa.pub 2>/dev/null + openssl rand -base64 32 > .ssh/token_key_passphrase + # Placeholder GitHub App key (real one not needed for core tests) + openssl genrsa -out .secrets/github-app.pem 2048 2>/dev/null + echo "Config files and SSH keys ready." + + # ------------------------------------------------------------------ + # Start the stack. --wait blocks until all healthchecks pass. + # No separate health-check polling needed. + # ------------------------------------------------------------------ - name: Start Minder Docker stack run: | - # Pull images first so startup is fast and errors are clear - docker compose -f docker-compose.minder.yaml pull - # --wait blocks until all service healthchecks pass + docker compose -f docker-compose.minder.yaml pull --quiet docker compose -f docker-compose.minder.yaml up -d --wait - env: - # bootstrap.sh waits independently, but --wait on compose - # means we already know both minder + keycloak are healthy here - KO_DOCKER_REPO: "" # not needed — using pre-pulled images # ------------------------------------------------------------------ - # Bootstrap: create test user in Keycloak, obtain offline token, - # trigger Minder user auto-enrollment, extract project ID. - # bootstrap.sh has its own wait_for_url loops as a safety net. + # Bootstrap: create test user, get offline token, enroll user, + # extract project ID. Script writes MINDER_PROJECT to $GITHUB_ENV. # ------------------------------------------------------------------ - name: Bootstrap test user and project run: ./scripts/bootstrap.sh @@ -113,7 +146,10 @@ jobs: CLIENT_ID: smoke-test-client # ------------------------------------------------------------------ - # Run Robot Framework tests directly on the runner (no container). + # Run Robot Framework tests directly on the runner. + # MINDER_PROJECT is set by bootstrap.sh via $GITHUB_ENV and is + # available here as a shell env var — do NOT use ${{ env.X }} syntax + # for vars set in previous steps (those are static at parse time). # ------------------------------------------------------------------ - name: Run smoke tests run: | @@ -131,9 +167,10 @@ jobs: env: MINDER_CONFIG: ${{ github.workspace }}/smoke-test-config.yaml MINDER_OFFLINE_TOKEN_PATH: ${{ github.workspace }}/offline.token - MINDER_PROJECT: ${{ env.MINDER_PROJECT }} MINDER_TEST_ORG: ${{ secrets.MINDER_TEST_ORG }} GH_TOKEN: ${{ secrets.GH_TOKEN }} + # MINDER_PROJECT is injected automatically from $GITHUB_ENV + # (set by bootstrap.sh) — no need to reference it here explicitly # ------------------------------------------------------------------ # Results From 57ec3f3f546c3eb7d5f277b9455e9019ad5f06a6 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Tue, 9 Jun 2026 13:03:58 +0530 Subject: [PATCH 4/6] fix(ci): guard teardown steps against missing compose file The teardown and log steps run with 'if: always()' and 'if: failure()' respectively, but docker-compose.minder.yaml is only created mid-workflow. If any earlier step (e.g. yq patch, curl) fails before the file is written, the teardown step crashes: open .../docker-compose.minder.yaml: no such file or directory Fix: check file existence before running docker compose commands in both the log-dump and teardown steps. This makes the cleanup path idempotent regardless of where in the workflow the failure occurred. Signed-off-by: jaydeep869 --- .github/workflows/integration-tests.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6790b33..4c48504 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -194,15 +194,24 @@ jobs: # ------------------------------------------------------------------ # Teardown + # All three steps guard against docker-compose.minder.yaml not + # existing (e.g. when an earlier step failed before creating it). # ------------------------------------------------------------------ - name: Show Minder logs on failure if: failure() - run: docker compose -f docker-compose.minder.yaml logs minder --tail=100 - - - name: Show Keycloak logs on failure - if: failure() - run: docker compose -f docker-compose.minder.yaml logs keycloak --tail=50 + run: | + if [ -f docker-compose.minder.yaml ]; then + docker compose -f docker-compose.minder.yaml logs minder --tail=100 + docker compose -f docker-compose.minder.yaml logs keycloak --tail=50 + else + echo "docker-compose.minder.yaml not found — stack was never started." + fi - name: Teardown Minder stack if: always() - run: docker compose -f docker-compose.minder.yaml down -v --remove-orphans + run: | + if [ -f docker-compose.minder.yaml ]; then + docker compose -f docker-compose.minder.yaml down -v --remove-orphans + else + echo "docker-compose.minder.yaml not found — nothing to tear down." + fi From 2bf3b2b95c4580c3dbb1108ed7042ecbb3d36d19 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Tue, 9 Jun 2026 13:05:58 +0530 Subject: [PATCH 5/6] fix(ci): correct minder CLI download URL and guard test reporter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. Wrong release asset name for minder CLI. The goreleaser archive name template is: minder_VERSION_Os_Arch (e.g. minder_v0.1.2_linux_amd64.tar.gz) The previous URL used 'minder_Linux_x86_64.tar.gz' which does not exist, causing curl to return a 404 HTML page piped into tar — hence 'gzip: stdin: not in gzip format'. Fix: include the version in the filename and use lowercase os/arch. 2. dorny/test-reporter fails hard when results/xoutput.xml does not exist (i.e. when the tests never ran due to an earlier failure). This masked the real failure with a secondary 'No test report files were found' error. Fix: add hashFiles() guard so the reporter only runs when the file exists, and set fail-on-error: false so a missing report does not override the real exit code. Signed-off-by: jaydeep869 --- .github/workflows/integration-tests.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4c48504..a6bbc9f 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -71,8 +71,9 @@ jobs: MINDER_VERSION=$(curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ | jq -r '.tag_name') echo "Downloading minder CLI ${MINDER_VERSION}..." - curl -sL \ - "https://github.com/mindersec/minder/releases/download/${MINDER_VERSION}/minder_Linux_x86_64.tar.gz" \ + # Goreleaser naming: minder_VERSION_linux_amd64.tar.gz + curl -fsSL \ + "https://github.com/mindersec/minder/releases/download/${MINDER_VERSION}/minder_${MINDER_VERSION}_linux_amd64.tar.gz" \ | tar -xz -C /usr/local/bin minder minder version @@ -184,13 +185,14 @@ jobs: retention-days: 30 - name: Publish test report - if: always() + # Only run if the results file was actually created (tests ran) + if: always() && hashFiles('results/xoutput.xml') != '' uses: dorny/test-reporter@v1 with: name: "Integration Test Results" path: results/xoutput.xml reporter: java-junit - fail-on-error: true + fail-on-error: false # ------------------------------------------------------------------ # Teardown From 5fdbbb07c3f3a9a8d106853c99da10decb451b90 Mon Sep 17 00:00:00 2001 From: jaydeep869 Date: Tue, 9 Jun 2026 13:07:22 +0530 Subject: [PATCH 6/6] fix(ci): use releases API to find minder CLI asset URL dynamically The goreleaser name template strips the 'v' prefix from the tag, so tag v0.1.2 produces minder_0.1.2_linux_amd64.tar.gz. The previous URL used the raw tag name (v0.1.2) in the path, resulting in a 404. Rather than hardcode the naming convention (which may change), query the releases API for .assets[] and filter by 'linux_amd64.tar.gz'. This approach is robust against any future goreleaser config changes. Also adds a diagnostic fallback that prints all available asset names if the expected asset is not found, making future failures easier to debug. Signed-off-by: jaydeep869 --- .github/workflows/integration-tests.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a6bbc9f..7260d1c 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -68,13 +68,24 @@ jobs: # ------------------------------------------------------------------ - name: Install minder CLI run: | - MINDER_VERSION=$(curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ - | jq -r '.tag_name') - echo "Downloading minder CLI ${MINDER_VERSION}..." - # Goreleaser naming: minder_VERSION_linux_amd64.tar.gz - curl -fsSL \ - "https://github.com/mindersec/minder/releases/download/${MINDER_VERSION}/minder_${MINDER_VERSION}_linux_amd64.tar.gz" \ - | tar -xz -C /usr/local/bin minder + # Fetch the download URL for the linux amd64 tarball directly from + # the releases API — avoids guessing the exact filename format + # (goreleaser strips the 'v' prefix: v0.1.2 -> minder_0.1.2_linux_amd64.tar.gz) + ASSET_URL=$(curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ + | jq -r '.assets[] + | select(.name | test("linux_amd64\\.tar\\.gz$")) + | .browser_download_url') + + if [ -z "${ASSET_URL}" ]; then + echo "ERROR: could not find a linux_amd64 tarball in the latest release assets." + echo "Available assets:" + curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ + | jq -r '.assets[].name' + exit 1 + fi + + echo "Downloading minder CLI from: ${ASSET_URL}" + curl -fsSL "${ASSET_URL}" | tar -xz -C /usr/local/bin minder minder version # ------------------------------------------------------------------