From 49bb196d31a036be7be8f2ba2b10eb940c76c9ed Mon Sep 17 00:00:00 2001 From: Albert Mink Date: Tue, 19 Mar 2024 15:31:37 +0100 Subject: [PATCH] Incompatible changes will lead to added labels and failing actions (#580) Co-authored-by: Michael Schneider --- .github/compatibility/.gitignore | 1 + .github/compatibility/json-schema-diff.js | 59 ++++++++++ .github/compatibility/package-lock.json | 125 ++++++++++++++++++++++ .github/compatibility/package.json | 16 +++ .github/workflows/json-compatibility.yml | 58 ++++++++++ 5 files changed, 259 insertions(+) create mode 100644 .github/compatibility/.gitignore create mode 100644 .github/compatibility/json-schema-diff.js create mode 100644 .github/compatibility/package-lock.json create mode 100644 .github/compatibility/package.json create mode 100644 .github/workflows/json-compatibility.yml diff --git a/.github/compatibility/.gitignore b/.github/compatibility/.gitignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/.github/compatibility/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.github/compatibility/json-schema-diff.js b/.github/compatibility/json-schema-diff.js new file mode 100644 index 000000000..af514a4be --- /dev/null +++ b/.github/compatibility/json-schema-diff.js @@ -0,0 +1,59 @@ +const difftool = require('json-schema-diff-validator') +const exec = require('@actions/exec'); +const core = require('@actions/core'); +const {readFileSync} = require('node:fs'); + + +let getChangedSchema = async () => { + let stdout = ''; + const options = { + listeners: { + stdout: (data) => { + stdout += data.toString(); + }, + stderr: (data) => { + core.error(data.toString()); + } + } + }; + + await exec.exec('git diff --name-only remotes/origin/main..HEAD', [], options); + + const pattern = new RegExp('file-formats/[a-z]{4}/[a-z]+-v[0-9]+\.json$', 'i'); + const lines = stdout.split("\n"); + const changedSchema = lines.filter(line => pattern.test(line)); + + return changedSchema; +} + +const processFile = async (file) => { + const dataNew = readFileSync(`../../${file}`, 'utf8'); + const schemaNew = JSON.parse(dataNew); + + try { + await exec.exec(`git checkout remotes/origin/main -- `, [file], { cwd: `../../`} ); + } catch (error) { + core.info(`File ${file} is not known to main branch.`); + // file is not on main branch, so we continue and compare the file to itself (no harm) + } + + try { + const dataOld = readFileSync(`../../${file}`, 'utf8'); + const schemaOld = JSON.parse(dataOld); + + difftool.validateSchemaCompatibility(schemaOld, schemaNew); + } catch (error) { + core.setFailed(error.toString()); + } +} + + +async function run() { + + const changedSchema = await getChangedSchema(); + for (const schema of changedSchema) { + await processFile(schema); + } +} + +run(); diff --git a/.github/compatibility/package-lock.json b/.github/compatibility/package-lock.json new file mode 100644 index 000000000..68abe6d05 --- /dev/null +++ b/.github/compatibility/package-lock.json @@ -0,0 +1,125 @@ +{ + "name": "json-schema-compatible", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "json-schema-compatible", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/exec": "^1.1.1", + "json-schema-diff-validator": "^0.4.1" + } + }, + "node_modules/@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz", + "integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + }, + "node_modules/fast-json-patch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz", + "integrity": "sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig==", + "dependencies": { + "fast-deep-equal": "^2.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" + }, + "node_modules/json-pointer": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", + "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "dependencies": { + "foreach": "^2.0.4" + } + }, + "node_modules/json-schema-diff-validator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-diff-validator/-/json-schema-diff-validator-0.4.1.tgz", + "integrity": "sha512-feIljaEKpucJozf3o3zCqhJ3q1RfTOTXr0RUXALuOhJ3vZ+kfmy3ZORgGJ7XsXQ+pwq6TZMfukivB8nxt3XmxA==", + "dependencies": { + "fast-json-patch": "^2.0.5", + "json-pointer": "^0.6.0" + }, + "bin": { + "json-schema-diff-validator": "dist/bin/cli.js" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/.github/compatibility/package.json b/.github/compatibility/package.json new file mode 100644 index 000000000..0ccd8b826 --- /dev/null +++ b/.github/compatibility/package.json @@ -0,0 +1,16 @@ +{ + "name": "json-schema-compatible", + "version": "1.0.0", + "description": "checks for incompatible changes in JSON Schema", + "main": "json-schema-diff.js", + "scripts": { + "check": "node json-schema-diff" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/exec": "^1.1.1", + "json-schema-diff-validator": "^0.4.1" + } +} diff --git a/.github/workflows/json-compatibility.yml b/.github/workflows/json-compatibility.yml new file mode 100644 index 000000000..76daae560 --- /dev/null +++ b/.github/workflows/json-compatibility.yml @@ -0,0 +1,58 @@ +name: Compatibility Check + +on: + pull_request: + +jobs: + compatible: + name: Is change incompatible + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + - name: Run script + id: check_incompatibility + run: | + cd .github/compatibility + npm ci + npm run check + + - name: Add label PR on failure + if: failure() && steps.check_incompatibility.outcome == 'failure' # Only runs if your script failed. + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'incompatible-changes', + color: 'FF0000' + }).catch(err => console.log(`Label already exists`)) + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['incompatible-changes'] + }) + github-token: ${{secrets.GITHUB_TOKEN}} + + - name: Remove label PR on success + if: steps.check_incompatibility.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'incompatible-changes' + }).then(() => { + console.log('Label removed successfully!'); + }).catch((error) => { + console.error('An error occurred while removing the label:', error); + }) + github-token: ${{secrets.GITHUB_TOKEN}} +