diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ca19921e758..b456993d777 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -551,6 +551,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /scripts/generate-rtk-apis.ts @grafana/grafana-frontend-platform /scripts/generate-alerting-rtk-apis.ts @grafana/alerting-frontend /scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend +/scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend /scripts/codemods/explicit-barrel-imports.cjs @grafana/frontend-ops /scripts/**/generate-transformations* @grafana/dataviz-squad diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index 778f29e694e..0cca7c5e785 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -8,7 +8,6 @@ on: - 'packages/**' branches: - 'main' - workflow_dispatch: jobs: buildPR: @@ -111,6 +110,9 @@ jobs: needs: ['buildPR', 'buildBase'] env: GITHUB_STEP_NUMBER: 8 + permissions: + contents: 'read' + id-token: 'write' steps: - uses: actions/checkout@v4 @@ -134,6 +136,29 @@ jobs: - name: Unzip artifact from base run: unzip -j base_built_packages.zip -d ./base && rm base_built_packages.zip + - id: 'auth' + uses: 'google-github-actions/auth@v2' + with: + workload_identity_provider: ${{ secrets.WIF_PROVIDER }} + service_account: ${{ secrets.LEVITATE_SA }} + + - name: 'Set up Cloud SDK' + uses: 'google-github-actions/setup-gcloud@v2' + with: + version: '>= 363.0.0' + project_id: 'grafanalabs-global' + install_components: 'bq' + + # This step is needed to generate a detailed levitate report + - name: Set up gcloud project + run: | + unset CLOUDSDK_CORE_PROJECT + unset GCLOUD_PROJECT + unset GCP_PROJECT + unset GOOGLE_CLOUD_PROJECT + + gcloud config set project grafanalabs-global + - name: Get link for the Github Action job id: job uses: actions/github-script@v6 diff --git a/scripts/levitate-parse-json-report.js b/scripts/levitate-parse-json-report.js index fd82fb34280..835e466180d 100644 --- a/scripts/levitate-parse-json-report.js +++ b/scripts/levitate-parse-json-report.js @@ -1,5 +1,7 @@ const fs = require('fs'); +const printAffectedPluginsSection = require('./levitate-show-affected-plugins'); + const data = JSON.parse(fs.readFileSync('data.json', 'utf8')); function stripAnsi(str) { @@ -28,4 +30,6 @@ if (data.changes.length > 0) { markdown += printSection('Changes', data.changes); } +markdown += printAffectedPluginsSection(data); + console.log(markdown); diff --git a/scripts/levitate-show-affected-plugins.js b/scripts/levitate-show-affected-plugins.js new file mode 100644 index 00000000000..17d619ca8b5 --- /dev/null +++ b/scripts/levitate-show-affected-plugins.js @@ -0,0 +1,149 @@ +/** + * @file This file exports the function `printAffectedPluginsSection` + * used to generate the affected plugins section in the report + * that is used in `levitate-parse-json-report.js` + */ + +const { execSync } = require('child_process'); + +/** + * Extracts the package name from a given location string. + * + * @param {string} location - The location string containing the package information. + * @returns {string} - The extracted package name, or an empty string if no match is found. + */ +function getPackage(location) { + const match = location.match(/\/(@[^@]+)@/); + return match ? match[1] : ''; +} + +const PANEL_URL = 'https://ops.grafana-ops.net/d/dmb2o0xnz/imported-property-details?orgId=1'; + +/** + * Creates an array of HTML links for the given section and affecting properties. + * + * @param {Array} section - An array of objects, each containing `name` and `location` properties. + * @param {Set} affectingProperties - A set of property names that are affected. + * @returns {Array} - An array of HTML link strings. + */ +function createLinks(section, affectingProperties) { + return section + .map(({ name, location }) => { + const package = getPackage(location); + + if (!package && !affectingProperties.has(name)) { + return undefined; + } + + const link = PANEL_URL + `&var-propertyName=${name}&var-packageName=${package}`; + + return `${package}/${name}`; + }) + .filter((item) => item !== undefined); +} + +/** + * Generates an SQL query to select property names, package names, and plugin IDs + * from the `plugin_imports` table based on the provided section data. + * + * @param {Array} section - An array of objects, each containing `name` and `location` properties. + * @returns {string} - The generated SQL query string. + */ +function makeQuery(section) { + const whereClause = section + .map(({ name, location }) => { + const package = getPackage(location); + + if (!package) { + return undefined; + } + + return `(property_name = '${name}' AND package_name = '${package}')`; + }) + .filter((item) => item !== undefined) + .join(' OR '); + + return ` + SELECT + property_name, + package_name, + plugin_id + FROM + \\\`grafanalabs-global.plugins_data.plugin_imports\\\` + WHERE ${whereClause} +`; +} + +/** + * Extracts a specific column from a table represented as an array of lines. + * + * @param {Array} lines - An array of strings, each representing a row in the table. + * @param {number} columnIndex - The index of the column to extract. + * @returns {Set} - A set containing the unique values from the specified column. + */ +function getColumn(lines, columnIndex) { + const set = new Set(); + const tableBody = lines.slice(3); + + for (let row of tableBody) { + const columns = row.split('|').map((col) => col.trim()); + + if (columns.length === 5) { + const content = columns[columnIndex]; + + set.add(content); + } + } + + return set; +} + + +/** + * Generates a markdown section detailing the affected plugins based on the provided data. + * + * @param {Object} data - The data object containing `removals` and `changes` arrays. + * @param {Array} data.removals - An array of objects representing removed items. + * @param {Array} data.changes - An array of objects representing changed items. + * @returns {string} - The generated markdown string detailing the affected plugins. + */ +function printAffectedPluginsSection(data) { + const { removals, changes } = data; + + let markdown = ''; + + try { + const sqlQuery = makeQuery([...removals, ...changes]); + const cmd = `bq query --nouse_legacy_sql "${sqlQuery}"`; + const stdout = execSync(cmd, { encoding: 'utf-8' }); + + const rows = stdout.trim().split('\n'); + + if (rows.length > 3) { + const pluginsColumnIndex = 3; + const affectedPlugins = getColumn(rows, pluginsColumnIndex); + + markdown += `

Number of affected plugins: ${affectedPlugins.size}

`; + markdown += "

To check the plugins affected by each import, click on the links below.

"; + + const propertiesColumnIndex = 1; + const affectingProperties = getColumn(rows, propertiesColumnIndex); + + if (removals.length > 0) { + markdown += `

Removals

`; + markdown += createLinks(removals, affectingProperties).join('
\n'); + } + + if (changes.length > 0) { + markdown += `

Changes

`; + markdown += createLinks(changes, affectingProperties).join('
\n'); + } + } + } catch (error) { + markdown += `

Error generating detailed report ${error}

`; + } + + return markdown; +} + +module.exports = printAffectedPluginsSection;