diff --git a/.github/workflows/adapter-naming.yml b/.github/workflows/adapter-naming.yml new file mode 100644 index 00000000000..171fbd54fcb --- /dev/null +++ b/.github/workflows/adapter-naming.yml @@ -0,0 +1,61 @@ +name: Check adapter naming conventions +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] +permissions: + contents: read +jobs: + check-names: + name: Check adapter naming conventions + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: install dependencies + uses: ./.github/actions/npm-ci + - name: Update metadata + id: update + continue-on-error: true + run: | + npx gulp update-metadata --no-fetch + - name: Adapter code does not match file name + if: ${{ steps.update.outcome != 'success' }} + run: | + echo '{"issue_number": ${{ github.event.pull_request.number }}, "body": "This PR includes an adapter whose code does not match its file name. Bid adapter modules should be named `BidAdapter`, userId `IdSystem`, RTD `RtdProvider`, and analytics `Analytics`."}' > ${{ runner.temp }}/comment.json + - name: Calculate diff + if: ${{ steps.update.outcome == 'success' }} + run: | + git diff --name-only $(git merge-base HEAD ${{ github.event.pull_request.base.sha }})..HEAD > ${{runner.temp}}/changed_files.txt + - name: Check naming + if: ${{ steps.update.outcome == 'success' }} + uses: actions/github-script@v9 + id: check + with: + result-encoding: string + script: | + const fs = require('fs'); + const { getViolationsSummary, formatViolationsSummary } = require('./metadata/validateNaming.mjs'); + const diff = fs.readFileSync('${{ runner.temp }}/changed_files.txt').toString().split('\n').map(s => s.trim()); + const modules = new Set(diff.map(filename => /^modules\/([^\/.]+)/.exec(filename)?.[1]).filter(fn => fn != null)); + const violations = Object.fromEntries( + Object.entries(await getViolationsSummary()) + .filter(([moduleName]) => modules.has(moduleName)) + ); + const formatted = formatViolationsSummary(violations); + if (formatted != null) { + fs.writeFileSync('${{ runner.temp }}/comment.json', JSON.stringify({ + issue_number: ${{ github.event.pull_request.number }}, + body: `Some adapters in this PR do not follow Prebid naming conventions.\n${formatted}` + })); + return 'true'; + } + return 'false'; + + - name: Upload comment data + if: ${{ steps.update.outcome != 'success' || steps.check.outputs.result == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: comment + path: ${{ runner.temp }}/comment.json diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml index 5fd8d272404..75eef1245b5 100644 --- a/.github/workflows/comment.yml +++ b/.github/workflows/comment.yml @@ -4,6 +4,7 @@ on: workflows: - Check for Duplicated Code - Check for linter warnings / exceptions + - Check adapter naming conventions types: - completed diff --git a/gulpfile.js b/gulpfile.js index c4059337063..4cb04e62664 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -578,3 +578,9 @@ gulp.task('compile-metadata', function (done) { }); gulp.task('update-metadata', gulp.series('build', 'extract-metadata', 'compile-metadata')); module.exports = nodeBundle; + +gulp.task('validate-names', function (done) { + import('./metadata/validateNaming.mjs').then(({ validateNaming }) => { + validateNaming().then(done, done); + }); +}); diff --git a/metadata/validateNaming.mjs b/metadata/validateNaming.mjs new file mode 100644 index 00000000000..beaf79e0eac --- /dev/null +++ b/metadata/validateNaming.mjs @@ -0,0 +1,109 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +async function readMetadata() { + return Object.fromEntries( + await Promise.all( + (await fs.readdir(path.resolve(import.meta.dirname, 'modules'))) + .map(async name => { + const components = JSON.parse((await fs.readFile(path.resolve(import.meta.dirname, 'modules', name))).toString()).components; + return [name.replace(/\.json$/, ''), components]; + }) + ) + ); +} + +function conflictDetector(metadata) { + function getKey({ componentType, componentName }) { + return componentType === 'bidder' ? `bidder.${componentName.substring(0, 6).toLowerCase()}` : `${componentType}.${componentName.toLowerCase()}`; + } + + const conflictMap = Object.entries(metadata).reduce((memo, [moduleName, components]) => { + components + .forEach(({ componentType, componentName, aliasOf }) => { + const key = getKey({ componentType, componentName }); + if (!memo.hasOwnProperty(key)) { + memo[key] = []; + } + memo[key].push({ + moduleName: moduleName, + componentType, + componentName, + aliasOf + }); + }); + return memo; + }, {}); + return function (moduleName, component) { + return conflictMap[getKey(component)]?.filter((entry) => entry.moduleName !== moduleName) ?? []; + }; +} + +function checkName({ componentType, componentName }) { + if (componentType === 'bidder' && !/^[a-z0-9_]+$/.test(componentName)) { + return 'contains uppercase or non-alphanumeric characters'; + } +} + +export async function getViolationsSummary() { + const meta = await readMetadata(); + const checkForConflicts = conflictDetector(meta); + return Object.entries(meta) + .reduce((memo, [moduleName, components]) => { + components.forEach(cmp => { + const conflicts = checkForConflicts(moduleName, cmp); + const nameViolation = checkName(cmp); + if (conflicts.length > 0 || nameViolation != null) { + if (!memo.hasOwnProperty(moduleName)) { + memo[moduleName] = []; + } + const entry = { + component: { + componentType: cmp.componentType, + componentName: cmp.componentName, + aliasOf: cmp.aliasOf + } + }; + if (conflicts.length > 0) { + entry.conflicts = conflicts; + } + if (nameViolation != null) { + entry.name = nameViolation; + } + memo[moduleName].push(entry); + } + }); + return memo; + }, {}); +} + +export function formatViolationsSummary(violations) { + const naming = []; + const conflicting = []; + + function declaration(component) { + return `${component.componentType} ${component.aliasOf ? 'alias' : 'code'} \`${component.componentName}\``; + } + + Object.entries(violations).forEach(([moduleName, entries]) => { + entries.forEach(({ component, name, conflicts }) => { + if (name) { + naming.push(`Module \`${moduleName}\` defines ${declaration(component)}, which ${name}`); + } + if (conflicts) { + conflicting.push(`* Module \`${moduleName}\` defines ${declaration(component)}, which conflicts with:`); + conflicts.forEach(conflict => conflicting.push(` * ${declaration(conflict)} defined in module \`${conflict.moduleName}\``)); + } + }); + }); + + return naming.concat(['']).concat(conflicting).join('\n'); +} + +export async function validateNaming() { + const warn = formatViolationsSummary(await getViolationsSummary()); + if (warn != null) { + console.warn(warn); + throw new Error('Some adapters do not follow naming conventions'); + } +} diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 800c58225f4..cfef047dac1 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -22,6 +22,7 @@ import { getAdUnitSizes } from '../libraries/sizeUtils/sizeUtils.js'; import { getBidFloor } from '../libraries/adkernelUtils/adkernelUtils.js'; /** + * touch * In case you're AdKernel whitelable platform's client who needs branded adapter to * work with Adkernel platform - DO NOT COPY THIS ADAPTER UNDER NEW NAME *