diff --git a/.github/scripts/check-capabilities-compatibility.js b/.github/scripts/check-capabilities-compatibility.js deleted file mode 100644 index f3e387e..0000000 --- a/.github/scripts/check-capabilities-compatibility.js +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs'); -const path = require('path'); - -function usage() { - console.error('Usage: node check-capabilities-compatibility.js --baseFile=capabilities.base.json --prFile=capabilities.json [--allowlist=.github/capabilities-compatibility-allowlist.json]'); - process.exit(2); -} - -const args = process.argv.slice(2).reduce((acc, cur) => { - const [k, v] = cur.split('='); - acc[k.replace(/^--/, '')] = v || true; - return acc; -}, {}); - -if (!args.baseFile || !args.prFile) usage(); -const baseFile = path.resolve(args.baseFile); -const prFile = path.resolve(args.prFile); -const allowlistFile = args.allowlist ? path.resolve(args.allowlist) : path.resolve('.github/capabilities-compatibility-allowlist.json'); - -function readJson(file) { - try { - const rawContent = fs.readFileSync(file, 'utf8'); - if (rawContent.length === 0) { - console.warn(`Warning: ${file} is empty, treating as empty object`); - return {}; - } - const content = rawContent.trim(); - if (!content) { - console.warn(`Warning: ${file} contains only whitespace, treating as empty object`); - return {}; - } - const parsed = JSON.parse(content); - if (parsed === null) { - console.warn(`Warning: ${file} contains JSON null, treating as empty object`); - return {}; - } - return parsed; - } catch (e) { - console.error(`Failed to read or parse JSON file: ${file}\n${e.message}`); - process.exit(2); - } -} - -const base = readJson(baseFile); -const pr = readJson(prFile); - -// Special case: if base is empty object (missing file), only validate PR structure -if (typeof base === 'object' && base !== null && Object.keys(base).length === 0) { - console.log('Base capabilities.json is missing - treating as new file addition.'); - console.log('Performing basic validation of new capabilities.json structure...'); - - // Basic validation for required properties in new capabilities.json - const requiredProps = ['dataRoles', 'dataViewMappings']; - const missingProps = requiredProps.filter(prop => !pr.hasOwnProperty(prop)); - if (missingProps.length > 0) { - console.error(`\nNew capabilities.json is missing required properties: ${missingProps.join(', ')}`); - process.exit(1); - } - - // Check for WebAccess (not allowed) - if (pr.privileges && pr.privileges.includes('WebAccess')) { - console.error('\nWebAccess privilege is not allowed in capabilities.json'); - process.exit(1); - } - - console.log('New capabilities.json structure is valid.'); - process.exit(0); -} - -let allowlist = []; -if (fs.existsSync(allowlistFile)) { - try { - const allowlistContent = fs.readFileSync(allowlistFile, 'utf8'); - allowlist = JSON.parse(allowlistContent); - } catch (e) { - console.warn('Warning: failed to parse allowlist, continuing without it'); - } -} - -function isPrimitive(val) { - return val === null || (typeof val !== 'object'); -} - -function pathJoin(parent, key) { - return parent ? `${parent}.${key}` : key; -} - -const issues = []; - -function record(path, message) { - // if allowlist contains exact path, skip - if (allowlist && Array.isArray(allowlist) && allowlist.includes(path)) return; - issues.push({ path, message }); -} - -function compareObjects(baseNode, prNode, parentPath) { - if (typeof baseNode !== typeof prNode) { - // Allow object vs array difference? report as modified - record(parentPath || '', `Type changed from ${typeof baseNode} to ${typeof prNode}`); - return; - } - - if (Array.isArray(baseNode)) { - // Heuristics: if array of objects and elements have `name` property, match by name. - if (baseNode.length > 0 && typeof baseNode[0] === 'object' && baseNode[0] !== null) { - const byName = baseNode[0] && Object.prototype.hasOwnProperty.call(baseNode[0], 'name'); - if (byName) { - const map = new Map(); - (prNode || []).forEach(item => { if (item && item.name) map.set(item.name, item); }); - baseNode.forEach((item, idx) => { - const key = item && item.name ? item.name : null; - const childPath = pathJoin(parentPath, `${idx}${key ? `(${key})` : ''}`); - if (key && !map.has(key)) { - record(childPath, `Array element with name='${key}' removed or renamed`); - } else if (key) { - compareObjects(item, map.get(key), pathJoin(parentPath, `name=${key}`)); - } else { - // fallback to index compare - const prItem = (prNode || [])[idx]; - if (prItem === undefined) record(childPath, `Array element at index ${idx} removed`); - else compareObjects(item, prItem, childPath); - } - }); - } else { - // compare by index - for (let i = 0; i < baseNode.length; i++) { - const childPath = pathJoin(parentPath, String(i)); - if (prNode.length <= i) { - record(childPath, `Array element at index ${i} removed`); - continue; - } - compareObjects(baseNode[i], prNode[i], childPath); - } - } - } else { - // base array of primitives - ensure not removed elements by index - for (let i = 0; i < baseNode.length; i++) { - const childPath = pathJoin(parentPath, String(i)); - if (!prNode || prNode.length <= i) record(childPath, `Array element at index ${i} removed`); - else if (typeof baseNode[i] !== typeof prNode[i]) record(childPath, `Type changed at array index ${i} from ${typeof baseNode[i]} to ${typeof prNode[i]}`); - } - } - return; - } - - if (isPrimitive(baseNode)) { - // primitive - only check type compatibility - if (isPrimitive(prNode) && typeof baseNode !== typeof prNode) { - record(parentPath || '', `Primitive type changed from ${typeof baseNode} to ${typeof prNode}`); - } - return; - } - - // both are objects - for (const key of Object.keys(baseNode)) { - const childPath = pathJoin(parentPath, key); - if (!Object.prototype.hasOwnProperty.call(prNode || {}, key)) { - record(childPath, `Property removed`); - continue; - } - compareObjects(baseNode[key], prNode[key], childPath); - } -} - -compareObjects(base, pr, ''); - -if (issues.length) { - console.error('\n=== capabilities.json compatibility issues detected ===\n'); - issues.forEach((it, i) => { - console.error(`${i + 1}. ${it.path} - ${it.message}`); - }); - console.error('\nIf these changes are intentional, add the exact JSON paths to the allowlist file (one per line) or update the baseline.'); - process.exit(1); -} - -console.log('capabilities.json structure is compatible with baseline. No breaking changes detected.'); -process.exit(0); diff --git a/.github/workflows/capabilities-compatibility.yml b/.github/workflows/capabilities-compatibility.yml deleted file mode 100644 index 97bcdc7..0000000 --- a/.github/workflows/capabilities-compatibility.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Capabilities compatibility check - -on: - pull_request: - -jobs: - check-capabilities: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetches all history to allow diffing against the base branch - - - name: Check for capabilities.json changes - id: check_changes - run: | - # Compare the PR branch with the base branch to find changed files - CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}) - echo "Files changed in this PR:" - echo "$CHANGED_FILES" - - if echo "$CHANGED_FILES" | grep -q "capabilities.json"; then - echo "capabilities.json was modified." - echo "any_changed=true" >> $GITHUB_OUTPUT - else - echo "capabilities.json was not modified. Skipping compatibility check." - echo "any_changed=false" >> $GITHUB_OUTPUT - fi - shell: bash - - - name: Determine base ref - env: - PR_BRANCH: ${{ github.head_ref }} - if: steps.check_changes.outputs.any_changed == 'true' - id: vars - run: | - echo "BASE_REF=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT - echo "PR_REF=$PR_BRANCH" >> $GITHUB_OUTPUT - - - name: Checkout base branch file - if: steps.check_changes.outputs.any_changed == 'true' - run: | - git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 - if git show origin/${{ github.event.pull_request.base.ref }}:capabilities.json > capabilities.base.json 2>/dev/null; then - echo "Base capabilities.json found" - else - echo "No capabilities.json in base branch - treating as new file" - echo '{}' > capabilities.base.json - fi - - - name: Run compatibility script - if: steps.check_changes.outputs.any_changed == 'true' - run: | - node ./.github/scripts/check-capabilities-compatibility.js --baseFile=capabilities.base.json --prFile=capabilities.json || exit 1 - shell: bash diff --git a/README.md b/README.md index 9457192..f954214 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,4 @@ The Bullet chart consists of 5 primary components: * Comparative Measure: The measure against which you want to compare your featured measure (eg: Target revenue). * Qualitative Scale: The background fill that encodes qualitative ranges like bad, satisfactory, and good. -See also [Bullet chart at Microsoft Office store](https://store.office.com/en-us/app.aspx?assetid=WA104380755&sourcecorrid=69216a8c-bd11-4cd0-9e5b-9c4e0469b74b&searchapppos=0&ui=en-US&rs=en-US&ad=US&appredirect=false) +See also [Bullet chart at Microsoft Office store](https://store.office.com/en-us/app.aspx?assetid=WA104380755&sourcecorrid=69216a8c-bd11-4cd0-9e5b-9c4e0469b74b&searchapppos=0&ui=en-US&rs=en-US&ad=US&appredirect=false) \ No newline at end of file