From 450a9246173cec2b2b3043cfca022f8f7f82eff0 Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 11:12:35 +0200 Subject: [PATCH 01/12] ci: add new gha for running frontend e2e tests against specific stack --- .../frontend-e2e-against-stack/.gitignore | 17 + .../frontend-e2e-against-stack/CHANGELOG.md | 5 + .../frontend-e2e-against-stack/README.md | 62 +++ .../frontend-e2e-against-stack/action.yml | 231 +++++++++ .../frontend-e2e-against-stack/package.json | 30 ++ .../plop-templates/docker-compose.hbs.yaml | 24 + .../frontend-e2e-against-stack/plopfile.mjs | 476 ++++++++++++++++++ 7 files changed, 845 insertions(+) create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/.gitignore create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/CHANGELOG.md create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/README.md create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/action.yml create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/package.json create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml create mode 100644 actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs diff --git a/actions/internal/plugins/frontend-e2e-against-stack/.gitignore b/actions/internal/plugins/frontend-e2e-against-stack/.gitignore new file mode 100644 index 00000000..479ff5ee --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/.gitignore @@ -0,0 +1,17 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +node_modules/ + +coverage/ +dist/ + +.*.bun-build + +# Editor +.idea diff --git a/actions/internal/plugins/frontend-e2e-against-stack/CHANGELOG.md b/actions/internal/plugins/frontend-e2e-against-stack/CHANGELOG.md new file mode 100644 index 00000000..6b3ab265 --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +- Create new action frontend-e2e-against-stack to run e2e playwright tests against pre-selected stack. diff --git a/actions/internal/plugins/frontend-e2e-against-stack/README.md b/actions/internal/plugins/frontend-e2e-against-stack/README.md new file mode 100644 index 00000000..cd573846 --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/README.md @@ -0,0 +1,62 @@ +# Run e2e tests from frontend plugins against specific stack + +This is a GitHub Action that help the execution of e2e tests on any frontend plugin that is using [Playwright](https://playwright.dev/) against specific selected stack. +You need to define in which region the selected stack belong, the plugin from where are executed the tests and optionally which other plugins and datasources you want to provision when starting a Grafana instance. +Also, you need to have the **playwright** configuration and the test specifications in the plugin that run the tests and the action will do the rest. +This action use the following input parameters to run: + +| Name | Description | Default | Required | +| --------------------- |--------------------------------------------------------------------------------------------------------------------|-------------------|----------| +| `plugin-directory` | Directory of the plugin, if not in the root of the repository. If provided, package-manager must also be provided. | . | No | +| `package-manager` | The package manager to use for building the plugin | | No | +| `npm-registry-auth` | Whether to authenticate to the npm registry in Google Artifact Registry | false | No | +| `stack_slug` | Name of the stack where you want to run the tests | | Yes | +| `env` | Region of the stack where you want to run the tests | | Yes | +| `other_plugins` | List of other plugins that you want to enable separated by comma | | No | +| `datasource_ids` | List of data sources that you want to enable separated by comma | | No | +| `upload_report_path ` | Name of the folder where you want to store the test report | playwright-report | No | +| `upload_videos_path` | Name of the folder where you want to store the test videos | playwright-videos | No | +| `plugin-secrets` | A JSON string containing key-value pairs of specific plugin secrets necessary to run the tests. | | No | +| `grafana-ini-path` | Path to a custom grafana.ini file to configure the Grafana instance | | No | + +## Example workflows + +This is an example of how you could use this action. + +```yml +name: Build and Test PR + +on: + pull_request: + +jobs: + e2e-tests: + permissions: + contents: write + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Get plugin specific secrets + id: create-plugin-secrets + shell: bash + run: | + echo 'plugin-json-secrets={"MY_SECRET1": "value_secrete_1", "MY_SECRET2": "value_secret_2"}' >> "$GITHUB_OUTPUT" + + - name: Run e2e cross app tests + id: e2e-cross-apps-tests + uses: grafana/plugin-ci-workflows/actions/internal/plugins/frontend-e2e-against-stack@main + with: + npm-registry-auth: "true" + stack_slug: "mygrafanastack" + env: "dev-central" + other_plugins: "grafana-plugin1-app,grafana-plugin2-app" + datasource_ids: "grafanacloud-mygrafanastack-prom,grafanacloud-mygrafanastack-logs" + upload_report_path: "playwright-cross-apps-report" + upload_videos_path: "playwright-cross-apps-videos" + plugin-secrets: ${{ steps.create-plugin-secrets.outputs.plugin-json-secrets }} + grafana-ini-path: "provisioning/custom-grafana.ini" # Optional +``` diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml new file mode 100644 index 00000000..0e9ba733 --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -0,0 +1,231 @@ +name: Run e2e tests +description: Run e2e tests against specific stack and environment +inputs: + plugin-directory: + description: Directory of the plugin, if not in the root of the repository. If provided, package-manager must also be provided. + required: false + default: . + + package-manager: + description: The package manager to use. + required: false + default: "" + + npm-registry-auth: + description: | + Whether to authenticate to the npm registry in Google Artifact Registry. + If true, the root of the plugin repository must contain a `.npmrc` file. + required: false + default: "false" + + stack_slug: + description: "Name of the stack where you want to run the tests" + required: true + + env: + description: "Region of the stack where you want to run the tests" + required: true + + other_plugins: + description: "List of other plugins that you want to enable separated by comma" + required: false + + datasource_ids: + description: "List of data sources that you want to enable separated by comma" + required: false + + upload_report_path: + description: "Name of the artifact where you want to store the test report" + required: false + default: "playwright-report" + + upload_videos_path: + description: "Name of the artifact where you want to store the test videos" + required: false + default: "playwright-videos" + + plugin-secrets: + description: "A JSON string containing key-value pairs of specific plugin secrets necessary to run the tests." + required: false + + grafana-ini-path: + description: "Path to a custom grafana.ini file to configure the Grafana instance. Path should be relative to the plugin directory." + required: false + default: "" + +runs: + using: "composite" + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Setup Node.js environment + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "20" + cache: "yarn" + + - name: Install e2e action dependencies + run: yarn install + shell: bash + + - name: Login to Google Cloud + id: gcloud-auth + if: inputs.npm-registry-auth == 'true' + uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + with: + token_format: access_token + workload_identity_provider: "projects/304398677251/locations/global/workloadIdentityPools/github/providers/github-provider" + service_account: "github-cloud-npm-dev-pkgs@grafanalabs-workload-identity.iam.gserviceaccount.com" + + - name: NPM registry auth + if: inputs.npm-registry-auth == 'true' + shell: bash + working-directory: ${{ inputs.plugin-directory }} + run: GOOGLE_APPLICATION_CREDENTIALS=${{ env.GOOGLE_APPLICATION_CREDENTIALS }} npx google-artifactregistry-auth --credential-config ./.npmrc + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.plugin-directory }} + run: ${{ github.action_path }}/pm.sh install + env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} + + - name: Build + shell: bash + working-directory: ${{ inputs.plugin-directory }} + run: ${{ github.action_path }}/pm.sh build + env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} + + # The action should end up with a dist/ folder, but if the working directory is not the root of the repo, + # we need to copy the dist/ folder to the root of the repo. + - name: Copy dist if needed + run: | + if [ "$PLUGIN_DIRECTORY" != "." ]; then + mkdir -p dist + cp -r $PLUGIN_DIRECTORY/dist/* dist/ + fi + shell: bash + if: inputs.plugin-directory != '.' + env: + PLUGIN_DIRECTORY: ${{ inputs.plugin-directory }} + + - name: Extract plugin ID from plugin.json + id: extract-plugin-id + shell: bash + working-directory: ${{ inputs.plugin-directory }} + run: | + if [ ! -f "src/plugin.json" ]; then + echo "Error: plugin.json not found at src/plugin.json" + exit 1 + fi + + if ! jq empty src/plugin.json 2>/dev/null; then + echo "Error: plugin.json contains invalid JSON" + exit 1 + fi + + PLUGIN_ID=$(jq -r '.id' src/plugin.json) + + if [ "$PLUGIN_ID" = "null" ] || [ -z "$PLUGIN_ID" ]; then + echo "Error: 'id' field not found or empty in plugin.json" + exit 1 + fi + + echo "plugin-id=$PLUGIN_ID" >> $GITHUB_OUTPUT + echo "Extracted plugin ID: $PLUGIN_ID" + + - name: Get common secrets + id: get-common-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@5d7e361bc7e0a183cde8afe9899fb7b596d2659b # v1.2.0 + with: + common_secrets: | + HG_TOKEN=hg-ci:token + + - name: Set plugin secrets as environment variables + id: set-env-vars + if: ${{ inputs.plugin-secrets != '' }} + shell: bash + env: + SECRETS_JSON: "${{ inputs.plugin-secrets }}" + run: | + echo "Parsing and setting plugin environment variables..." + echo "$SECRETS_JSON" | jq -r 'to_entries[] | "echo \"\(.key)=\(.value)\" >> $GITHUB_ENV"' | bash + echo "Plugin environment variables set." + + - name: Generate provisioning + shell: bash + run: npx plop e2e-testing-provisioning + working-directory: ${{ inputs.plugin-directory }} + env: + E2E_STACK_SLUG: ${{ inputs.stack_slug }} + E2E_ENV: ${{ inputs.env }} + HG_TOKEN: ${{ env.HG_TOKEN }} + E2E_PLUGIN_ID: ${{ steps.extract-plugin-id.outputs.plugin-id }} + E2E_OTHER_PLUGINS: ${{ inputs.other_plugins }} + E2E_DATASOURCE_IDS: ${{ inputs.datasource_ids }} + E2E_GRAFANA_INI_PATH: ${{ inputs.grafana-ini-path }} + + - name: Start server + run: docker compose up -d --build --quiet-pull --timestamps + working-directory: ${{ inputs.plugin-directory }} + shell: bash + + - name: Install Playwright Browsers + run: npx playwright install chromium --with-deps + working-directory: ${{ inputs.plugin-directory }} + shell: bash + + - name: Run Playwright tests + shell: bash + env: + NODE_ENV: production + run: | + echo "Waiting for Grafana to be available..." + timeout=300 # 5 minutes timeout + start_time=$(date +%s) + + while ! docker logs grafana-csp-app 2>&1 | grep "Usage stats are ready to report" > /dev/null; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + + if [ $elapsed -ge $timeout ]; then + echo "Timeout reached: Grafana did not become ready within 5 minutes." + exit 1 + fi + + echo "Waiting for Grafana..." + sleep 5 # Wait for 5 seconds before checking again + done + + echo "Grafana is ready!" + npx playwright test + working-directory: ${{ inputs.plugin-directory }} + + - name: Stop grafana docker + run: docker compose down + working-directory: ${{ inputs.plugin-directory }} + shell: bash + + - name: Upload E2E report + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ inputs.upload_report_path }} + path: playwright-report/ + retention-days: 30 + + - name: Upload E2E videos + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ inputs.upload_videos_path }} + path: test-results/ + retention-days: 30 + +branding: + icon: "shield" + color: "green" diff --git a/actions/internal/plugins/frontend-e2e-against-stack/package.json b/actions/internal/plugins/frontend-e2e-against-stack/package.json new file mode 100644 index 00000000..be075bc4 --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/package.json @@ -0,0 +1,30 @@ +{ + "name": "plugins-e2e-tests", + "version": "0.0.1", + "description": "Run e2e tests from plugins against specific stack and environment.", + "private": true, + "scripts": {}, + "keywords": [ + "e2e", + "github-action", + "playwright" + ], + "author": "", + "license": "ISC", + "dependencies": { + "js-yaml": "^4.1.0", + "plop": "^4.0.1" + }, + "devDependencies": { + "@types/bun": "1.2.22", + "@eslint/js": "9.35.0", + "@types/eslint__js": "9.14.0", + "eslint": "9.35.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-jest": "29.0.1", + "eslint-plugin-prettier": "5.5.4", + "prettier": "3.6.2", + "typescript": "5.9.2", + "typescript-eslint": "8.44.0" + } +} diff --git a/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml b/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml new file mode 100644 index 00000000..b639ff5c --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml @@ -0,0 +1,24 @@ +services: + grafana: + environment: + - GF_PLUGINS_PREINSTALL_SYNC={{{GF_PLUGINS_PREINSTALL_SYNC}}} + - GF_GRAFANA_COM_SSO_API_TOKEN={{{GF_GRAFANA_COM_SSO_API_TOKEN}}} + - GF_GRAFANA_COM_URL={{{GF_GRAFANA_COM_URL}}} + - GF_GRAFANA_COM_API_URL={{{GF_GRAFANA_COM_API_URL}}} + + container_name: "{{{GF_PLUGIN_ID}}}" + restart: on-failure + platform: "linux/amd64" + build: + context: ./.config + args: + grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} + grafana_version: ${GRAFANA_VERSION:-nightly} + ports: + - 3000:3000/tcp + volumes: + - ./dist:/var/lib/grafana/plugins/{{{GF_PLUGIN_ID}}} + - ./provisioning:/etc/grafana/provisioning + {{#if GRAFANA_INI_PATH}} + - ./{{{GRAFANA_INI_PATH}}}:/etc/grafana/grafana.ini + {{/if}} diff --git a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs new file mode 100644 index 00000000..68c87f0c --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs @@ -0,0 +1,476 @@ +import fs from "fs"; +import yaml from "js-yaml"; +import * as path from "path"; + +function isError(response) { + if (!response || typeof response !== "object") { + return false; + } + const keys = Object.keys(response); + return keys.includes("code") || keys.includes("message"); +} + +/** + * Checks if a value is null, undefined, an empty array, or an object with no enumerable properties. + * @param {any} value The value to check. + * @returns {boolean} True if the value is empty, otherwise false. + */ +function isEmpty(value) { + // console.log('Check is empty for: ', JSON.stringify(value)); + if (value === null || typeof value === "undefined") { + return true; + } + if (Array.isArray(value)) { + return value.length === 0; + } + if (typeof value === "string") { + return value.length === 0; + } + if (typeof value === "object") { + return Object.keys(value).length === 0; + } + return false; +} + +const HG_TOKEN = process.env.HG_TOKEN; +const APPS_YAML_FILE = path.join( + process.cwd(), + "./provisioning/plugins/apps.yaml", +); +const DATASOURCES_YAML_FILE = path.join( + process.cwd(), + "./provisioning/datasources/default.yaml", +); +const HG_REGION_SUFFIX_MAP = { + "prod-us-east": "prod-us-east-0", + "prod-eu-west": "prod-eu-west-0", + prod: "prod-us-central-0", + ops: "ops-eu-south-0", + "dev-east": "dev-us-east-0", + "dev-central": "dev-us-central-0", +}; + +const gcloudDSPattern = /grafanacloud-(\w+)-([a-z-]+)/; + +/** + * getProvisionedDSType returns the provisioned datasource type and returns an empty string if it doesn't match the pattern and the slug. + * @param datasourceName The full name of the datasource (e.g., "grafanacloud-my-slug-traces"). + * @param slug The expected slug (e.g., "my-slug"). + * @returns The datasource type (e.g., "traces"), or an empty string if criteria are not met. + */ +function getProvisionedDSType(datasourceName, slug) { + const match = gcloudDSPattern.exec(datasourceName); + if (match && match.length >= 3 && match[1] === slug) { + return match[2]; + } + return ""; +} + +/** + * Creates a predictable UID for Grafana Cloud datasources. + * If the datasource matches the pattern (e.g., grafanacloud--), the UID is simplified to "grafanacloud-"; otherwise, it uses the full name. + * @param dataSource The dataSource provisioned object + * @param stackSlug The expected slug (e.g., "staging"). + * @returns A UID string, guaranteed to be 40 characters or less. + */ +function getUid(dataSource, stackSlug) { + const datasourceName = dataSource.name; + let uid = datasourceName; + + const provisionedDSType = getProvisionedDSType(datasourceName, stackSlug); + if (provisionedDSType !== "") { + uid = "grafanacloud-" + provisionedDSType; + } + const maxLength = 40; + if (uid.length > maxLength) { + uid = uid.slice(uid.length - maxLength); + } + return uid; +} + +function formatDataSource(dataSource, stackSlug) { + if (dataSource) { + const uid = !dataSource.uid + ? getUid(dataSource, stackSlug) + : dataSource.uid; + return { + name: dataSource.name, + type: dataSource.type, + ...(uid && { uid }), + url: dataSource.url, + basicAuth: dataSource.basicAuth === 1 || dataSource.basicAuth === true, + basicAuthUser: dataSource.basicAuthUser + ? Number(dataSource.basicAuthUser) + : undefined, + isDefault: dataSource.isDefault === 1 || dataSource.isDefault === true, + jsonData: dataSource.jsonData, + secureJsonData: { + basicAuthPassword: dataSource.basicAuthPassword, + }, + }; + } + return dataSource; +} + +function removeEmptyProperties(obj) { + if (!obj || isEmpty(obj)) { + return obj; + } + // Check if the input is an object or an array + if (Array.isArray(obj)) { + // If it's an array, recursively clean each element + return obj + .map((item) => removeEmptyProperties(item)) + .filter((item) => item !== null && typeof item !== "undefined"); + } + + // Check if the input is a plain object + if (typeof obj === "object" && obj !== null) { + const newObj = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const value = obj[key]; + + // Recursively clean nested objects/arrays + const cleanedValue = removeEmptyProperties(value); + + // Check for empty values and skip them + if ( + cleanedValue !== "" && + cleanedValue !== null && + cleanedValue !== undefined && + !(Array.isArray(cleanedValue) && cleanedValue.length === 0) && + !( + typeof cleanedValue === "object" && + Object.keys(cleanedValue).length === 0 + ) + ) { + newObj[key] = cleanedValue; + } + } + } + return newObj; + } + + // If the value is not an object or array, return it as is + return obj; +} + +function isProdEnvironment(env) { + const envType = env.split("-")[0]; + return envType === "prod"; +} + +/** + * Dynamically generates the HG base API URL based on the region selected. + * @param {string} env The environment region (e.g., "prod-us-east", "dev-central"). + * @returns {string} The constructed base API URL. + */ +function getBaseUrlByEnv(env) { + const envType = env.split("-")[0]; + + let domainSuffix = "grafana"; + if (["dev", "ops"].includes(envType)) { + domainSuffix = `grafana-${envType}`; + } + const regionSuffix = + HG_REGION_SUFFIX_MAP[env] || HG_REGION_SUFFIX_MAP["dev-central"]; + return `https://hg-api-${regionSuffix}.${domainSuffix}.net`; +} + +async function fetchMultipleAppConfigs(stackSlug, env, pluginIds) { + try { + const fetchPromises = pluginIds.map((pluginId) => + fetchAppConfig(stackSlug, env, pluginId), + ); + return await Promise.all(fetchPromises); + } catch (error) { + console.error("Error fetching multiple app configs:", error.message); + throw error; + } +} + +async function fetchAppConfig(stackSlug, env, pluginId) { + try { + const baseUrl = getBaseUrlByEnv(env); + const url = `${baseUrl}/instances/${stackSlug}/provisioned-plugins/${pluginId}`; + + const response = await fetch(url, { + headers: { + "User-Agent": `plop/${pluginId}-provisioning`, + Authorization: `Bearer ${HG_TOKEN}`, + }, + }); + return response.json(); + } catch (error) { + console.error("Error fetching app config", pluginId, ":", error.message); + throw error; + } +} + +async function fetchMultipleDatasources(stackSlug, env, datasourceNames) { + try { + const fetchPromises = datasourceNames.map((dsName) => + fetchDataSource(stackSlug, env, dsName), + ); + if (fetchPromises.length > 0) { + return Promise.all(fetchPromises); + } + return Promise.all([]); + } catch (error) { + console.error("Error fetching multiple data sources:", error.message); + throw error; + } +} + +async function fetchDataSource(stackSlug, env, datasourceName) { + try { + const baseUrl = getBaseUrlByEnv(env); + const url = `${baseUrl}/instances/${stackSlug}/datasources/${datasourceName}`; + const response = await fetch(url, { + headers: { + "User-Agent": `plop/${datasourceName}-provisioning`, + Authorization: `Bearer ${HG_TOKEN}`, + }, + }); + const dataSourceWithToken = await response.json(); + const dataSourceWithNoEmptyField = + removeEmptyProperties(dataSourceWithToken); + return formatDataSource(dataSourceWithNoEmptyField, stackSlug); + } catch (error) { + console.error( + "Error fetching datasource", + datasourceName, + ":", + error.message, + ); + throw error; + } +} + +function createDataSourcesYamlFile() { + const dir = path.dirname(DATASOURCES_YAML_FILE); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const initialContent = { + apiVersion: 1, + prune: true, + + datasources: [], + }; + fs.writeFileSync(DATASOURCES_YAML_FILE, yaml.dump(initialContent)); + return initialContent; +} + +async function fetchGrafanaConfig(stackSlug, env, pluginId) { + try { + const baseUrl = getBaseUrlByEnv(env); + const url = `${baseUrl}/instances/${stackSlug}/config`; + const response = await fetch(url, { + headers: { + "User-Agent": `plop/${pluginId}-provisioning`, + Authorization: `Bearer ${HG_TOKEN}`, + }, + }); + return response.json(); + } catch (error) { + console.error("Error fetching gcom token:", error.message); + throw error; + } +} + +function createAppsYamlFile() { + const dir = path.dirname(APPS_YAML_FILE); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const initialContent = { + apiVersion: 1, + + apps: [], + }; + fs.writeFileSync(APPS_YAML_FILE, yaml.dump(initialContent)); + return initialContent; +} + +function addAppConfigs(yamlData, appConfigs) { + appConfigs.forEach((appConfig) => { + if (appConfig.type === "grafana-asserts-app") { + appConfig.jsonData.instanceUrl = "http://localhost:3000"; + } + yamlData.apps.push(appConfig); + console.log(`App with type '${appConfig.type}' has been added`); + }); +} + +function addDataSourceConfigs(yamlData, dataSourceConfigs = []) { + dataSourceConfigs.forEach((dsConfig, i) => { + yamlData.datasources.push(dsConfig); + console.log( + `Data source with type '${dsConfig.type}' and name '${dsConfig.name}' has been added`, + ); + }); +} + +function writeDataSourcesYamlFile(yamlData) { + const yamlString = yaml.dump(yamlData); + fs.writeFileSync(DATASOURCES_YAML_FILE, yamlString); + console.log("default.yaml data source file has been updated."); +} + +function writeAppsYamlFile(yamlData) { + const yamlString = yaml.dump(yamlData); + + // just for asserts + const fixed = yamlString.replace( + "enableGrafanaManagedLLM: true", + "enableGrafanaManagedLLM: false", + ); + fs.writeFileSync(APPS_YAML_FILE, fixed); + console.log( + "apps.yaml plugins file has been updated. Asserts prop enableGrafanaManagedLLM was disabled", + ); +} + +async function fillAnswers(answers) { + if (isProdEnvironment(answers.ENV)) { + console.error( + "For security reason, you are not allowed to provision plugins locally on production environment.", + ); + process.exit(1); + } + if (!answers.PLUGIN_IDS || answers.PLUGIN_IDS.length === 0) { + console.error( + `No plugin was selected for the stack ${answers.STACK_SLUG} on environment ${answers.ENV}.`, + ); + process.exit(1); + } + if (!answers.DATASOURCE_IDS || answers.DATASOURCE_IDS.length === 0) { + console.error( + `No data source selected for the stack ${answers.STACK_SLUG} on environment ${answers.ENV}.`, + ); + process.exit(1); + } + + const appConfigs = await fetchMultipleAppConfigs( + answers.STACK_SLUG, + answers.ENV, + answers.PLUGIN_IDS, + ); + if (isError(appConfigs) || isEmpty(appConfigs)) { + console.error( + `No app config found for the stack ${answers.STACK_SLUG} on environment ${answers.ENV}.`, + ); + process.exit(1); + } + const yamlAppsData = createAppsYamlFile(); + addAppConfigs(yamlAppsData, appConfigs); + writeAppsYamlFile(yamlAppsData); + + const dataSourceConfigs = await fetchMultipleDatasources( + answers.STACK_SLUG, + answers.ENV, + answers.DATASOURCE_IDS, + ); + if (isError(dataSourceConfigs) || isEmpty(dataSourceConfigs)) { + console.error( + `The data sources ${answers.DATASOURCE_IDS} cannot be loaded from the stack ${answers.STACK_SLUG} on environment ${answers.ENV}.`, + ); + process.exit(1); + } + const yamlDataSourcesData = createDataSourcesYamlFile(); + addDataSourceConfigs(yamlDataSourcesData, dataSourceConfigs); + writeDataSourcesYamlFile(yamlDataSourcesData); + + const grafanaConfig = await fetchGrafanaConfig( + answers.STACK_SLUG, + answers.ENV, + answers.GF_PLUGIN_ID, + ); + if (isError(grafanaConfig) || isEmpty(grafanaConfig)) { + console.error( + `No grafana config found for plugin ${answers.GF_PLUGIN_ID} on the stack ${answers.STACK_SLUG} and environment ${answers.ENV}.`, + ); + process.exit(1); + } + answers.GF_GRAFANA_COM_SSO_API_TOKEN = + grafanaConfig.hosted_grafana.hg_auth_token; + + // Use hardcoded URL for ops stack when grafana_net.url is missing + const grafanaNetUrl = + answers.STACK_SLUG === "ops" && !grafanaConfig.grafana_net?.url + ? "https://grafana-ops.com" + : grafanaConfig.grafana_net.url; + + answers.GF_GRAFANA_COM_URL = grafanaNetUrl; + answers.GF_GRAFANA_COM_API_URL = `${grafanaNetUrl}/api`; + answers.GF_PLUGINS_PREINSTALL_SYNC = answers.PLUGIN_IDS.filter( + (p) => p !== answers.GF_PLUGIN_ID, + ).join(","); +} + +export default function (plop) { + plop.setHelper("env", (text) => process.env[text]); + + plop.setGenerator("e2e-testing-provisioning", { + prompts: [], + actions: [ + async function loadRemoteProvisioning(answers) { + try { + if (!HG_TOKEN) { + console.error("HG_TOKEN environment variable is not set."); + process.exit(1); + } + + if (!process.env.E2E_STACK_SLUG) { + console.error("E2E_STACK_SLUG environment variable is not set."); + process.exit(1); + } + + if (!process.env.E2E_PLUGIN_ID) { + console.error("E2E_PLUGIN_ID environment variable is not set."); + process.exit(1); + } + answers.STACK_SLUG = process.env.E2E_STACK_SLUG; + + if (isProdEnvironment(process.env.E2E_ENV)) { + throw "For security reason, you are not allowed to provision locally production environment."; + } + answers.ENV = process.env.E2E_ENV; + + answers.GF_PLUGIN_ID = process.env.E2E_PLUGIN_ID; + const otherPlugins = process.env.E2E_OTHER_PLUGINS + ? process.env.E2E_OTHER_PLUGINS.split(",").map((i) => i.trim()) + : []; + answers.PLUGIN_IDS = [process.env.E2E_PLUGIN_ID].concat(otherPlugins); + + answers.DATASOURCE_IDS = process.env.E2E_DATASOURCE_IDS + ? process.env.E2E_DATASOURCE_IDS.split(",").map((i) => i.trim()) + : []; + answers.GF_PLUGINS_PREINSTALL_SYNC = otherPlugins.join(","); + + // Handle custom grafana.ini file path + answers.GRAFANA_INI_PATH = process.env.E2E_GRAFANA_INI_PATH || ""; + + await fillAnswers(answers); + + return "Remote Provisioning data loaded successfully for e2e tests."; + } catch (error) { + console.error("Failed to load Remote Provisioning:", error.message); + throw error; + } + }, + { + type: "add", + path: "./docker-compose.yaml", + templateFile: "plop-templates/docker-compose.hbs.yaml", + force: true, + }, + ], + }); +} From 8d83c6fd807b42e3a53ba22e6a52366d40fa0179 Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 11:46:28 +0200 Subject: [PATCH 02/12] ci: add missing pm.sh script into the new action --- .../plugins/frontend-e2e-against-stack/pm.sh | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 actions/internal/plugins/frontend-e2e-against-stack/pm.sh diff --git a/actions/internal/plugins/frontend-e2e-against-stack/pm.sh b/actions/internal/plugins/frontend-e2e-against-stack/pm.sh new file mode 100755 index 00000000..fc7cea6f --- /dev/null +++ b/actions/internal/plugins/frontend-e2e-against-stack/pm.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Check if command argument is provided +if [ "$1" = "" ]; then + echo "Please provide a command to run." + exit 1 +fi + +install_pnpm_if_not_present() { + if ! command -v pnpm &> /dev/null + then + echo "pnpm could not be found, installing..." + npm install -g pnpm + fi +} + +# Use provided package manager if set in PACKAGE_MANAGER environment variable +if [ -n "$PACKAGE_MANAGER" ]; then + pm="$PACKAGE_MANAGER" +# Detect the package manager +elif [ -f yarn.lock ]; then + pm="yarn" +elif [ -f pnpm-lock.yaml ]; then + install_pnpm_if_not_present + pm="pnpm" +elif [ -f package-lock.json ]; then + pm="npm" +else + echo "No recognized package manager found in this project." + exit 1 +fi + +# Run the provided command with the detected package manager +echo "Running '$1' with $pm..." +if [ "$1" = "install" ]; then + "$pm" install +else + "$pm" run "$1" +fi From 1a1ba036922ef3e15086395e5ffef20fb35e435f Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 11:55:36 +0200 Subject: [PATCH 03/12] ci: define location of plopfile --- actions/internal/plugins/frontend-e2e-against-stack/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index 0e9ba733..a501445b 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -158,7 +158,7 @@ runs: - name: Generate provisioning shell: bash - run: npx plop e2e-testing-provisioning + run: npx plop --plopfile ${{ github.action_path }}/plopfile.mjs e2e-testing-provisioning working-directory: ${{ inputs.plugin-directory }} env: E2E_STACK_SLUG: ${{ inputs.stack_slug }} From a93a87a1fe03d9b179e36bfa029dc5e7b73132ad Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 12:02:13 +0200 Subject: [PATCH 04/12] ci: define location of gha node_modules during Install e2e action dependencies step --- actions/internal/plugins/frontend-e2e-against-stack/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index a501445b..84ea4e92 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -70,6 +70,7 @@ runs: - name: Install e2e action dependencies run: yarn install shell: bash + working-directory: ${{ github.action_path }} - name: Login to Google Cloud id: gcloud-auth From eb98c34ef90b4e58120ec5baf05589bdaa308ec7 Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 12:18:14 +0200 Subject: [PATCH 05/12] ci: rename docker compose file to avoid conflict with possible existing file in the consumer repo --- .../internal/plugins/frontend-e2e-against-stack/action.yml | 6 +++--- .../plugins/frontend-e2e-against-stack/plopfile.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index 84ea4e92..0b969086 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -171,7 +171,7 @@ runs: E2E_GRAFANA_INI_PATH: ${{ inputs.grafana-ini-path }} - name: Start server - run: docker compose up -d --build --quiet-pull --timestamps + run: docker compose -f docker-compose.e2e.yaml up -d --build --quiet-pull --timestamps working-directory: ${{ inputs.plugin-directory }} shell: bash @@ -189,7 +189,7 @@ runs: timeout=300 # 5 minutes timeout start_time=$(date +%s) - while ! docker logs grafana-csp-app 2>&1 | grep "Usage stats are ready to report" > /dev/null; do + while ! docker logs ${{ steps.extract-plugin-id.outputs.plugin-id }} 2>&1 | grep "Usage stats are ready to report" > /dev/null; do current_time=$(date +%s) elapsed=$((current_time - start_time)) @@ -207,7 +207,7 @@ runs: working-directory: ${{ inputs.plugin-directory }} - name: Stop grafana docker - run: docker compose down + run: docker compose -f docker-compose.e2e.yaml down working-directory: ${{ inputs.plugin-directory }} shell: bash diff --git a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs index 68c87f0c..e3281aa3 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs +++ b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs @@ -467,7 +467,7 @@ export default function (plop) { }, { type: "add", - path: "./docker-compose.yaml", + path: "./docker-compose.e2e.yaml", templateFile: "plop-templates/docker-compose.hbs.yaml", force: true, }, From 5c2cf62a82f6772a09a5ad3866161d10dafca4cc Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 12:31:39 +0200 Subject: [PATCH 06/12] ci: store docker-compose.e2e.yaml file in plugin consumer directory --- .../internal/plugins/frontend-e2e-against-stack/plopfile.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs index e3281aa3..86f36c70 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs +++ b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs @@ -467,7 +467,7 @@ export default function (plop) { }, { type: "add", - path: "./docker-compose.e2e.yaml", + path: "docker-compose.e2e.yaml", templateFile: "plop-templates/docker-compose.hbs.yaml", force: true, }, From 73c3906d95f7173473138d400090e3fc67fc025b Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 12:40:40 +0200 Subject: [PATCH 07/12] ci: use dest plop flag which tells plop to create files in the current directory (which is the plugin directory). --- actions/internal/plugins/frontend-e2e-against-stack/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index 0b969086..b2000fe1 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -159,7 +159,7 @@ runs: - name: Generate provisioning shell: bash - run: npx plop --plopfile ${{ github.action_path }}/plopfile.mjs e2e-testing-provisioning + run: npx plop --plopfile ${{ github.action_path }}/plopfile.mjs --dest . e2e-testing-provisioning working-directory: ${{ inputs.plugin-directory }} env: E2E_STACK_SLUG: ${{ inputs.stack_slug }} From acd8a431b037ee13dc8978eacabec40a07d4738d Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 13:59:03 +0200 Subject: [PATCH 08/12] ci: use grafana health check to determine if the instance is ready or not --- .../frontend-e2e-against-stack/action.yml | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index b2000fe1..9ed25213 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -180,30 +180,48 @@ runs: working-directory: ${{ inputs.plugin-directory }} shell: bash - - name: Run Playwright tests + - name: Wait for Grafana to be ready shell: bash - env: - NODE_ENV: production run: | echo "Waiting for Grafana to be available..." timeout=300 # 5 minutes timeout start_time=$(date +%s) - while ! docker logs ${{ steps.extract-plugin-id.outputs.plugin-id }} 2>&1 | grep "Usage stats are ready to report" > /dev/null; do + while true; do current_time=$(date +%s) elapsed=$((current_time - start_time)) if [ $elapsed -ge $timeout ]; then echo "Timeout reached: Grafana did not become ready within 5 minutes." + echo "Container logs:" + docker logs ${{ steps.extract-plugin-id.outputs.plugin-id }} --tail 50 exit 1 fi - echo "Waiting for Grafana..." - sleep 5 # Wait for 5 seconds before checking again + # Check if Grafana is responding to health checks + if curl -f -s http://localhost:3000/api/health > /dev/null 2>&1; then + echo "Grafana health endpoint is responding!" + + # Additional check: ensure the API is fully ready + if curl -f -s http://localhost:3000/api/org > /dev/null 2>&1; then + echo "Grafana is fully ready!" + break + else + echo "Grafana health OK, but API not fully ready yet..." + fi + else + echo "Waiting for Grafana... (${elapsed}s elapsed)" + fi + + sleep 5 done + working-directory: ${{ inputs.plugin-directory }} - echo "Grafana is ready!" - npx playwright test + - name: Run Playwright tests + shell: bash + env: + NODE_ENV: production + run: npx playwright test working-directory: ${{ inputs.plugin-directory }} - name: Stop grafana docker From 147982ef217a775bb09422b77f93c9567f5dcbfb Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 14:15:52 +0200 Subject: [PATCH 09/12] ci: use also grafana plugins api to check if plugins are available --- .../frontend-e2e-against-stack/action.yml | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index 9ed25213..d4e6edf9 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -184,9 +184,20 @@ runs: shell: bash run: | echo "Waiting for Grafana to be available..." - timeout=300 # 5 minutes timeout + timeout=480 # 8 minutes timeout start_time=$(date +%s) + # Parse expected plugins from environment variables + expected_plugins="${{ steps.extract-plugin-id.outputs.plugin-id }}" + if [ -n "${{ inputs.other_plugins }}" ]; then + expected_plugins="$expected_plugins,${{ inputs.other_plugins }}" + fi + + # Convert comma-separated list to array for easier processing + IFS=',' read -ra PLUGIN_ARRAY <<< "$expected_plugins" + + echo "Expected plugins: $expected_plugins" + while true; do current_time=$(date +%s) elapsed=$((current_time - start_time)) @@ -195,25 +206,55 @@ runs: echo "Timeout reached: Grafana did not become ready within 5 minutes." echo "Container logs:" docker logs ${{ steps.extract-plugin-id.outputs.plugin-id }} --tail 50 + echo "Installed plugins:" + curl -s http://localhost:3000/api/plugins 2>/dev/null | jq -r '.[].id' 2>/dev/null || echo "Could not fetch plugins list" exit 1 fi # Check if Grafana is responding to health checks if curl -f -s http://localhost:3000/api/health > /dev/null 2>&1; then - echo "Grafana health endpoint is responding!" + echo "Grafana health endpoint is responding! (${elapsed}s elapsed)" - # Additional check: ensure the API is fully ready + # Check if API is ready if curl -f -s http://localhost:3000/api/org > /dev/null 2>&1; then - echo "Grafana is fully ready!" - break + echo "Grafana API is ready! Now checking plugins..." + + # Get list of installed plugins + installed_plugins=$(curl -s http://localhost:3000/api/plugins 2>/dev/null | jq -r '.[].id' 2>/dev/null) + + if [ $? -eq 0 ] && [ -n "$installed_plugins" ]; then + # Check if all expected plugins are installed + all_plugins_ready=true + missing_plugins="" + + for expected_plugin in "${PLUGIN_ARRAY[@]}"; do + # Trim whitespace + expected_plugin=$(echo "$expected_plugin" | xargs) + if [ -n "$expected_plugin" ]; then + if ! echo "$installed_plugins" | grep -q "^${expected_plugin}$"; then + all_plugins_ready=false + missing_plugins="$missing_plugins $expected_plugin" + fi + fi + done + + if [ "$all_plugins_ready" = true ]; then + echo "All expected plugins are installed and ready!" + break + else + echo "Still waiting for plugins:$missing_plugins (${elapsed}s elapsed)" + fi + else + echo "Could not fetch plugins list, retrying... (${elapsed}s elapsed)" + fi else - echo "Grafana health OK, but API not fully ready yet..." + echo "Grafana health OK, but API not fully ready yet... (${elapsed}s elapsed)" fi else - echo "Waiting for Grafana... (${elapsed}s elapsed)" + echo "Waiting for Grafana health endpoint... (${elapsed}s elapsed)" fi - sleep 5 + sleep 10 done working-directory: ${{ inputs.plugin-directory }} From 89e5fa952722020f13f6518efb596f02d86b66e8 Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 14:52:16 +0200 Subject: [PATCH 10/12] ci: define just the provisioned files generated not the whole folder to avoid conflict --- .../plop-templates/docker-compose.hbs.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml b/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml index b639ff5c..0add6dfd 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml +++ b/actions/internal/plugins/frontend-e2e-against-stack/plop-templates/docker-compose.hbs.yaml @@ -18,7 +18,8 @@ services: - 3000:3000/tcp volumes: - ./dist:/var/lib/grafana/plugins/{{{GF_PLUGIN_ID}}} - - ./provisioning:/etc/grafana/provisioning + - ./provisioning/datasources/default.yaml:/etc/grafana/provisioning/datasources/default.yaml + - ./provisioning/plugins/apps.yaml:/etc/grafana/provisioning/plugins/apps.yaml {{#if GRAFANA_INI_PATH}} - ./{{{GRAFANA_INI_PATH}}}:/etc/grafana/grafana.ini {{/if}} From 3fde201b313d47859ef4d3cfbce3561f7009e66c Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 15:04:53 +0200 Subject: [PATCH 11/12] ci: fix zyzmor errors --- .../frontend-e2e-against-stack/action.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/action.yml b/actions/internal/plugins/frontend-e2e-against-stack/action.yml index d4e6edf9..c79c949e 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/action.yml +++ b/actions/internal/plugins/frontend-e2e-against-stack/action.yml @@ -85,7 +85,9 @@ runs: if: inputs.npm-registry-auth == 'true' shell: bash working-directory: ${{ inputs.plugin-directory }} - run: GOOGLE_APPLICATION_CREDENTIALS=${{ env.GOOGLE_APPLICATION_CREDENTIALS }} npx google-artifactregistry-auth --credential-config ./.npmrc + env: + GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} + run: npx google-artifactregistry-auth --credential-config ./.npmrc - name: Install dependencies shell: bash @@ -182,15 +184,18 @@ runs: - name: Wait for Grafana to be ready shell: bash + env: + MAIN_PLUGIN_ID: ${{ steps.extract-plugin-id.outputs.plugin-id }} + OTHER_PLUGINS: ${{ inputs.other_plugins }} run: | echo "Waiting for Grafana to be available..." - timeout=480 # 8 minutes timeout + timeout=300 # 5 minutes timeout start_time=$(date +%s) - # Parse expected plugins from environment variables - expected_plugins="${{ steps.extract-plugin-id.outputs.plugin-id }}" - if [ -n "${{ inputs.other_plugins }}" ]; then - expected_plugins="$expected_plugins,${{ inputs.other_plugins }}" + # Parse expected plugins from environment variables (safely) + expected_plugins="$MAIN_PLUGIN_ID" + if [ -n "$OTHER_PLUGINS" ]; then + expected_plugins="$expected_plugins,$OTHER_PLUGINS" fi # Convert comma-separated list to array for easier processing @@ -205,7 +210,7 @@ runs: if [ $elapsed -ge $timeout ]; then echo "Timeout reached: Grafana did not become ready within 5 minutes." echo "Container logs:" - docker logs ${{ steps.extract-plugin-id.outputs.plugin-id }} --tail 50 + docker logs "$MAIN_PLUGIN_ID" --tail 50 echo "Installed plugins:" curl -s http://localhost:3000/api/plugins 2>/dev/null | jq -r '.[].id' 2>/dev/null || echo "Could not fetch plugins list" exit 1 From 2b289f6a171b45aeabc15870f80665f5ee0f0761 Mon Sep 17 00:00:00 2001 From: yduartep Date: Wed, 22 Oct 2025 15:43:08 +0200 Subject: [PATCH 12/12] ci: use standard word datasource instead of dataSource --- .../frontend-e2e-against-stack/plopfile.mjs | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs index 86f36c70..77a319a0 100644 --- a/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs +++ b/actions/internal/plugins/frontend-e2e-against-stack/plopfile.mjs @@ -69,12 +69,12 @@ function getProvisionedDSType(datasourceName, slug) { /** * Creates a predictable UID for Grafana Cloud datasources. * If the datasource matches the pattern (e.g., grafanacloud--), the UID is simplified to "grafanacloud-"; otherwise, it uses the full name. - * @param dataSource The dataSource provisioned object + * @param datasource The datasource provisioned object * @param stackSlug The expected slug (e.g., "staging"). * @returns A UID string, guaranteed to be 40 characters or less. */ -function getUid(dataSource, stackSlug) { - const datasourceName = dataSource.name; +function getUid(datasource, stackSlug) { + const datasourceName = datasource.name; let uid = datasourceName; const provisionedDSType = getProvisionedDSType(datasourceName, stackSlug); @@ -88,28 +88,28 @@ function getUid(dataSource, stackSlug) { return uid; } -function formatDataSource(dataSource, stackSlug) { - if (dataSource) { - const uid = !dataSource.uid - ? getUid(dataSource, stackSlug) - : dataSource.uid; +function formatDatasource(datasource, stackSlug) { + if (datasource) { + const uid = !datasource.uid + ? getUid(datasource, stackSlug) + : datasource.uid; return { - name: dataSource.name, - type: dataSource.type, + name: datasource.name, + type: datasource.type, ...(uid && { uid }), - url: dataSource.url, - basicAuth: dataSource.basicAuth === 1 || dataSource.basicAuth === true, - basicAuthUser: dataSource.basicAuthUser - ? Number(dataSource.basicAuthUser) + url: datasource.url, + basicAuth: datasource.basicAuth === 1 || datasource.basicAuth === true, + basicAuthUser: datasource.basicAuthUser + ? Number(datasource.basicAuthUser) : undefined, - isDefault: dataSource.isDefault === 1 || dataSource.isDefault === true, - jsonData: dataSource.jsonData, + isDefault: datasource.isDefault === 1 || datasource.isDefault === true, + jsonData: datasource.jsonData, secureJsonData: { - basicAuthPassword: dataSource.basicAuthPassword, + basicAuthPassword: datasource.basicAuthPassword, }, }; } - return dataSource; + return datasource; } function removeEmptyProperties(obj) { @@ -211,7 +211,7 @@ async function fetchAppConfig(stackSlug, env, pluginId) { async function fetchMultipleDatasources(stackSlug, env, datasourceNames) { try { const fetchPromises = datasourceNames.map((dsName) => - fetchDataSource(stackSlug, env, dsName), + fetchDatasource(stackSlug, env, dsName), ); if (fetchPromises.length > 0) { return Promise.all(fetchPromises); @@ -223,7 +223,7 @@ async function fetchMultipleDatasources(stackSlug, env, datasourceNames) { } } -async function fetchDataSource(stackSlug, env, datasourceName) { +async function fetchDatasource(stackSlug, env, datasourceName) { try { const baseUrl = getBaseUrlByEnv(env); const url = `${baseUrl}/instances/${stackSlug}/datasources/${datasourceName}`; @@ -233,10 +233,10 @@ async function fetchDataSource(stackSlug, env, datasourceName) { Authorization: `Bearer ${HG_TOKEN}`, }, }); - const dataSourceWithToken = await response.json(); - const dataSourceWithNoEmptyField = - removeEmptyProperties(dataSourceWithToken); - return formatDataSource(dataSourceWithNoEmptyField, stackSlug); + const datasourceWithToken = await response.json(); + const datasourceWithNoEmptyField = + removeEmptyProperties(datasourceWithToken); + return formatDatasource(datasourceWithNoEmptyField, stackSlug); } catch (error) { console.error( "Error fetching datasource", @@ -248,7 +248,7 @@ async function fetchDataSource(stackSlug, env, datasourceName) { } } -function createDataSourcesYamlFile() { +function createDatasourcesYamlFile() { const dir = path.dirname(DATASOURCES_YAML_FILE); if (!fs.existsSync(dir)) { @@ -308,8 +308,8 @@ function addAppConfigs(yamlData, appConfigs) { }); } -function addDataSourceConfigs(yamlData, dataSourceConfigs = []) { - dataSourceConfigs.forEach((dsConfig, i) => { +function addDatasourceConfigs(yamlData, datasourceConfigs = []) { + datasourceConfigs.forEach((dsConfig, i) => { yamlData.datasources.push(dsConfig); console.log( `Data source with type '${dsConfig.type}' and name '${dsConfig.name}' has been added`, @@ -317,7 +317,7 @@ function addDataSourceConfigs(yamlData, dataSourceConfigs = []) { }); } -function writeDataSourcesYamlFile(yamlData) { +function writeDatasourcesYamlFile(yamlData) { const yamlString = yaml.dump(yamlData); fs.writeFileSync(DATASOURCES_YAML_FILE, yamlString); console.log("default.yaml data source file has been updated."); @@ -372,20 +372,20 @@ async function fillAnswers(answers) { addAppConfigs(yamlAppsData, appConfigs); writeAppsYamlFile(yamlAppsData); - const dataSourceConfigs = await fetchMultipleDatasources( + const datasourceConfigs = await fetchMultipleDatasources( answers.STACK_SLUG, answers.ENV, answers.DATASOURCE_IDS, ); - if (isError(dataSourceConfigs) || isEmpty(dataSourceConfigs)) { + if (isError(datasourceConfigs) || isEmpty(datasourceConfigs)) { console.error( `The data sources ${answers.DATASOURCE_IDS} cannot be loaded from the stack ${answers.STACK_SLUG} on environment ${answers.ENV}.`, ); process.exit(1); } - const yamlDataSourcesData = createDataSourcesYamlFile(); - addDataSourceConfigs(yamlDataSourcesData, dataSourceConfigs); - writeDataSourcesYamlFile(yamlDataSourcesData); + const yamlDatasourcesData = createDatasourcesYamlFile(); + addDatasourceConfigs(yamlDatasourcesData, datasourceConfigs); + writeDatasourcesYamlFile(yamlDatasourcesData); const grafanaConfig = await fetchGrafanaConfig( answers.STACK_SLUG,