testing using the uffizzi workflow
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
committed by
web-next-automation
parent
0b3fac608a
commit
7cd15860dc
@@ -0,0 +1,111 @@
|
||||
name: API Breaking Changes (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- 'API Breaking Changes (Trigger)'
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Add values from previous step
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
# "If you specify the access for any of these scopes, all of those that are not specified are set to none."
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
actions: read # Access cache
|
||||
outputs:
|
||||
git-ref: ${{ steps.event.outputs.GIT_REF }}
|
||||
pr-number: ${{ steps.event.outputs.PR_NUMBER }}
|
||||
action: ${{ steps.event.outputs.ACTION }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
|
||||
- name: 'Download artifacts'
|
||||
# Fetch output (zip archive) from the workflow run that triggered this workflow.
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "preview-spec"
|
||||
})[0];
|
||||
if (matchArtifact === undefined) {
|
||||
throw TypeError('Build Artifact not found!');
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
|
||||
|
||||
- name: 'Accept event from first stage'
|
||||
run: unzip preview-spec.zip event.json
|
||||
|
||||
- name: Read Event into ENV
|
||||
id: event
|
||||
run: |
|
||||
echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_OUTPUT
|
||||
echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT
|
||||
echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: DEBUG - Print Job Outputs
|
||||
if: ${{ runner.debug }}
|
||||
run: |
|
||||
echo "PR number: ${{ steps.event.outputs.PR_NUMBER }}"
|
||||
echo "Git Ref: ${{ steps.event.outputs.GIT_REF }}"
|
||||
echo "Action: ${{ steps.event.outputs.ACTION }}"
|
||||
cat event.json
|
||||
|
||||
- name: Get Comment
|
||||
id: get-comment
|
||||
run: |
|
||||
unzip preview-spec.zip comment.md
|
||||
ls
|
||||
echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT
|
||||
|
||||
add-comment:
|
||||
name: Write comment about issues
|
||||
needs:
|
||||
- setup
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
# Identify comment to be updated
|
||||
- name: Find comment for Ephemeral Environment
|
||||
uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ needs.cache-manifests-file.outputs.pr-number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: pr-changes-${{ needs.cache-manifests-file.outputs.pr-number }}
|
||||
direction: last
|
||||
|
||||
- name: Create or Update Comment with Deployment URL
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{ steps.notification.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: comment.md
|
||||
edit-mode: replace
|
||||
@@ -0,0 +1,62 @@
|
||||
name: API Breaking Changes (Trigger)
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
paths-ignore:
|
||||
- '.changeset/**'
|
||||
- 'contrib/**'
|
||||
- 'docs/**'
|
||||
- 'microsite/**'
|
||||
- 'beps/**'
|
||||
- 'scripts/**'
|
||||
- 'storybook/**'
|
||||
- '**/*.test.*'
|
||||
- '**/package.json'
|
||||
- '*.md'
|
||||
|
||||
jobs:
|
||||
get-backstage-changes:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
name: Build PR image
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
|
||||
outputs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@a674369920067381b450d398b27df7039b7ef635 # v0.6.5
|
||||
with:
|
||||
cache-prefix: linux-v18
|
||||
|
||||
- name: breaking changes check
|
||||
run: |
|
||||
yarn backstage-repo-tools repo schema openapi check > comment.md
|
||||
|
||||
- name: Upload Rendered Comment as Artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: comment.md
|
||||
retention-days: 2
|
||||
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: ${{ github.event_path }}
|
||||
retention-days: 2
|
||||
@@ -5,3 +5,4 @@ Portions of this software were developed by third-party software vendors:
|
||||
|
||||
- Tech Radar Plugin (https://opensource.zalando.com/tech-radar/), Copyright (c) 2017 Zalando SE
|
||||
- [OpenAPI Generator Templates](./packages/repo-tools/templates), Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) Copyright 2018 SmartBear Software
|
||||
- Optic CLI (https://github.com/opticdev/optic), Copyright 2022, Optic Labs Corporation
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@stoplight/spectral-rulesets": "^1.18.0",
|
||||
"@stoplight/spectral-runtime": "^1.1.2",
|
||||
"@stoplight/types": "^14.0.0",
|
||||
"@useoptic/openapi-utilities": "^0.54.8",
|
||||
"chalk": "^4.0.0",
|
||||
"codeowners-utils": "^1.0.2",
|
||||
"command-exists": "^1.2.9",
|
||||
|
||||
@@ -78,6 +78,14 @@ function registerPackageCommand(program: Command) {
|
||||
.action(
|
||||
lazy(() => import('./package/schema/openapi/fuzz').then(m => m.command)),
|
||||
);
|
||||
|
||||
openApiCommand
|
||||
.command('check')
|
||||
.option('--ignore', 'Ignore linting failures and only log the results.')
|
||||
.option('--json', 'Output the results as JSON')
|
||||
.action(
|
||||
lazy(() => import('./package/schema/openapi/check').then(m => m.command)),
|
||||
);
|
||||
}
|
||||
|
||||
function registerRepoCommand(program: Command) {
|
||||
@@ -96,11 +104,7 @@ function registerRepoCommand(program: Command) {
|
||||
openApiCommand
|
||||
.command('verify [paths...]')
|
||||
.description(
|
||||
'Verify that all OpenAPI schemas are valid and set up correctly. This also verifies that your API has not changed in a breaking way.',
|
||||
)
|
||||
.option(
|
||||
'--from <ref>',
|
||||
'The base ref to compare against. Defaults to the fork point of the current branch.',
|
||||
'Verify that all OpenAPI schemas are valid and set up correctly.',
|
||||
)
|
||||
.action(
|
||||
lazy(() =>
|
||||
@@ -137,6 +141,20 @@ function registerRepoCommand(program: Command) {
|
||||
.action(
|
||||
lazy(() => import('./repo/schema/openapi/fuzz').then(m => m.command)),
|
||||
);
|
||||
|
||||
openApiCommand
|
||||
.command('check')
|
||||
.description(
|
||||
'Check the repository against a specific ref, will run all package `check:api` scripts.',
|
||||
)
|
||||
.option(
|
||||
'--since <ref>',
|
||||
'Check the API against a specific ref',
|
||||
'origin/master',
|
||||
)
|
||||
.action(
|
||||
lazy(() => import('./repo/schema/openapi/check').then(m => m.command)),
|
||||
);
|
||||
}
|
||||
|
||||
export function registerCommands(program: Command) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import chalk from 'chalk';
|
||||
import { exec } from '../../../../lib/exec';
|
||||
import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers';
|
||||
import { paths as cliPaths } from '../../../../lib/paths';
|
||||
import { OptionValues } from 'commander';
|
||||
import { env } from 'process';
|
||||
import { readFile, rm } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const reduceOpticOutput = (output: string) => {
|
||||
return output
|
||||
.split('\n')
|
||||
.filter(e => !e.startsWith('Rerun') && e.trim())
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
async function check(opts: OptionValues) {
|
||||
const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec();
|
||||
|
||||
let baseRef = opts.since ?? process.env.GITHUB_BASE_REF;
|
||||
if (!baseRef) {
|
||||
const { stdout: branch } = await exec(
|
||||
'git merge-base --fork-point origin/master',
|
||||
);
|
||||
baseRef = branch.toString().trim();
|
||||
}
|
||||
|
||||
let failed = false;
|
||||
let output = '';
|
||||
try {
|
||||
const { stdout } = await exec(
|
||||
'yarn optic diff',
|
||||
[
|
||||
resolvedOpenapiPath,
|
||||
'--check',
|
||||
opts.json ? '--json' : '',
|
||||
'--base',
|
||||
baseRef,
|
||||
],
|
||||
{
|
||||
cwd: cliPaths.targetRoot,
|
||||
env: { CI: opts.json ? '1' : undefined, ...env },
|
||||
},
|
||||
);
|
||||
output = stdout.toString();
|
||||
} catch (err) {
|
||||
output = err.stdout;
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
const file = (
|
||||
await readFile(resolve(cliPaths.targetRoot, 'ci-run-details.json'))
|
||||
).toString();
|
||||
const results = JSON.parse(file);
|
||||
console.log(file);
|
||||
if (!opts.ignore && results.failed) {
|
||||
throw new Error('Some checks failed');
|
||||
}
|
||||
|
||||
await rm(resolve(cliPaths.targetRoot, 'ci-run-details.json'));
|
||||
} else {
|
||||
console.log(reduceOpticOutput(output));
|
||||
if (!opts.ignore && failed) {
|
||||
throw new Error('Some checks failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues) {
|
||||
try {
|
||||
await check(opts);
|
||||
if (!opts.json) console.log(chalk.green(`All checks passed.`));
|
||||
} catch (err) {
|
||||
if (!opts.json) console.log(chalk.red(err.message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { OptionValues } from 'commander';
|
||||
import { exec } from '../../../../lib/exec';
|
||||
import {
|
||||
CiRunDetails,
|
||||
generateCompareSummaryMarkdown,
|
||||
} from '../../../../lib/openapi/optic/helpers';
|
||||
|
||||
export async function command(opts: OptionValues) {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
if (opts.since) {
|
||||
const graph = PackageGraph.fromPackages(packages);
|
||||
const changedPackages = await graph.listChangedPackages({
|
||||
ref: opts.since,
|
||||
analyzeLockfile: true,
|
||||
});
|
||||
const withDevDependents = graph.collectPackageNames(
|
||||
changedPackages.map(pkg => pkg.name),
|
||||
pkg => pkg.localDevDependents.keys(),
|
||||
);
|
||||
packages = Array.from(withDevDependents).map(name => graph.get(name)!);
|
||||
}
|
||||
|
||||
const checkablePackages = packages.filter(
|
||||
e => e.packageJson.scripts?.['check:api'],
|
||||
);
|
||||
try {
|
||||
const outputs = {
|
||||
completed: [],
|
||||
failed: [],
|
||||
noop: [],
|
||||
severity: 0,
|
||||
} as CiRunDetails;
|
||||
for (const pkg of checkablePackages) {
|
||||
const { stdout } = await exec(
|
||||
'yarn',
|
||||
['check:api', '--ignore', '--json'],
|
||||
{
|
||||
cwd: pkg.dir,
|
||||
},
|
||||
);
|
||||
const result = JSON.parse(stdout.toString());
|
||||
outputs.completed.push(...(result.completed ?? []));
|
||||
outputs.failed.push(...(result.failed ?? []));
|
||||
outputs.noop.push(...(result.noop ?? []));
|
||||
}
|
||||
|
||||
const { stdout: currentSha } = await exec('git', ['rev-parse', 'HEAD']);
|
||||
console.log(
|
||||
generateCompareSummaryMarkdown(
|
||||
{ sha: currentSha.toString().trim() },
|
||||
outputs,
|
||||
{ verbose: true },
|
||||
),
|
||||
);
|
||||
|
||||
const failed = outputs.failed.length > 0;
|
||||
if (failed) {
|
||||
throw new Error('Some checks failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -29,17 +29,14 @@ import {
|
||||
YAML_SCHEMA_PATH,
|
||||
} from '../../../../lib/openapi/constants';
|
||||
import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers';
|
||||
import { exec } from '../../../../lib/exec';
|
||||
import { OptionValues } from 'commander';
|
||||
|
||||
async function verify(directoryPath: string, options: OptionValues) {
|
||||
let openapiPath = '';
|
||||
try {
|
||||
openapiPath = await getPathToOpenApiSpec(directoryPath);
|
||||
} catch {
|
||||
// Unable to find spec at path.
|
||||
return;
|
||||
}
|
||||
const verifySpecAndGeneratedSpecMatch = async (
|
||||
openapiPath: string,
|
||||
directoryPath: string,
|
||||
) => {
|
||||
const openapiTempDirectory = resolvePath(cliPaths.targetDir, '.openapi');
|
||||
await fs.mkdirp(openapiTempDirectory);
|
||||
console.log(openapiTempDirectory);
|
||||
|
||||
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
|
||||
await Parser.validate(cloneDeep(yaml) as any);
|
||||
@@ -60,39 +57,22 @@ async function verify(directoryPath: string, options: OptionValues) {
|
||||
`\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools package schema openapi generate\` from '${path}' to regenerate \`${TS_SCHEMA_PATH}\`.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let baseRef = options.from ?? process.env.GITHUB_BASE_REF;
|
||||
if (!baseRef) {
|
||||
const { stdout: branch } = await exec('git merge-base --fork-point HEAD');
|
||||
baseRef = branch.toString().trim();
|
||||
}
|
||||
|
||||
async function verify(directoryPath: string) {
|
||||
let openapiPath = '';
|
||||
try {
|
||||
const { stdout } = await exec('optic diff', [
|
||||
openapiPath,
|
||||
'--check',
|
||||
'--base',
|
||||
baseRef,
|
||||
]);
|
||||
// Log out the results as this still shows API changes that aren't breakages.
|
||||
console.log(
|
||||
stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(e => !e.startsWith('Rerun') && e.trim())
|
||||
.join('\n'),
|
||||
);
|
||||
} catch (err) {
|
||||
err.message = err.stdout;
|
||||
throw err;
|
||||
openapiPath = await getPathToOpenApiSpec(directoryPath);
|
||||
} catch {
|
||||
// Unable to find spec at path.
|
||||
return;
|
||||
}
|
||||
|
||||
await verifySpecAndGeneratedSpecMatch(openapiPath, directoryPath);
|
||||
}
|
||||
|
||||
export async function bulkCommand(
|
||||
paths: string[] = [],
|
||||
options: OptionValues,
|
||||
): Promise<void> {
|
||||
const resultsList = await runner(paths, dir => verify(dir, options));
|
||||
export async function bulkCommand(paths: string[] = []): Promise<void> {
|
||||
const resultsList = await runner(paths, dir => verify(dir));
|
||||
|
||||
let failed = false;
|
||||
for (const { relativeDir, resultText } of resultsList) {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
import {
|
||||
compareSpecs,
|
||||
groupDiffsByEndpoint,
|
||||
Severity,
|
||||
getOperationsChangedLabel,
|
||||
getOperationsChanged,
|
||||
} from '@useoptic/openapi-utilities';
|
||||
import { GroupedDiffs } from '@useoptic/openapi-utilities/build/openapi3/group-diff';
|
||||
import { relative } from 'path';
|
||||
import { paths as cliPaths } from '../../paths';
|
||||
|
||||
type Comparison = {
|
||||
groupedDiffs: ReturnType<typeof groupDiffsByEndpoint>;
|
||||
results: Awaited<ReturnType<typeof compareSpecs>>['results'];
|
||||
};
|
||||
|
||||
export type CiRunDetails = {
|
||||
completed: {
|
||||
warnings: string[];
|
||||
apiName: string;
|
||||
opticWebUrl?: string | null;
|
||||
comparison: Comparison;
|
||||
specUrl?: string | null;
|
||||
capture?: any;
|
||||
}[];
|
||||
failed: { apiName: string; error: string }[];
|
||||
noop: { apiName: string }[];
|
||||
severity: Severity;
|
||||
};
|
||||
|
||||
const getChecksLabel = (
|
||||
results: CiRunDetails['completed'][number]['comparison']['results'],
|
||||
severity: Severity,
|
||||
) => {
|
||||
const totalChecks = results.length;
|
||||
let failingChecks = 0;
|
||||
let exemptedFailingChecks = 0;
|
||||
|
||||
for (const result of results) {
|
||||
if (result.passed) continue;
|
||||
if (result.severity < severity) continue;
|
||||
if (result.exempted) exemptedFailingChecks += 1;
|
||||
else failingChecks += 1;
|
||||
}
|
||||
|
||||
const exemptedChunk =
|
||||
exemptedFailingChecks > 0 ? `, ${exemptedFailingChecks} exempted` : '';
|
||||
|
||||
return failingChecks > 0
|
||||
? `⚠️ **${failingChecks}**/**${totalChecks}** failed${exemptedChunk}`
|
||||
: totalChecks > 0
|
||||
? `✅ **${totalChecks}** passed${exemptedChunk}`
|
||||
: `ℹ️ No automated checks have run`;
|
||||
};
|
||||
|
||||
function getOperationsText(
|
||||
groupedDiffs: GroupedDiffs,
|
||||
options: { webUrl?: string | null; verbose: boolean; labelJoiner?: string },
|
||||
) {
|
||||
const ops = getOperationsChanged(groupedDiffs);
|
||||
|
||||
const operationsText = options.verbose
|
||||
? [
|
||||
...[...ops.added].map(o => `\`${o}\` (added)`),
|
||||
...[...ops.changed].map(o => `\`${o}\` (changed)`),
|
||||
...[...ops.removed].map(o => `\`${o}\` (removed)`),
|
||||
].join('\n')
|
||||
: '';
|
||||
return `${getOperationsChangedLabel(groupedDiffs, {
|
||||
joiner: options.labelJoiner,
|
||||
})}
|
||||
|
||||
${operationsText}
|
||||
`;
|
||||
}
|
||||
|
||||
const getCaptureIssuesLabel = ({
|
||||
unmatchedInteractions,
|
||||
mismatchedEndpoints,
|
||||
}: {
|
||||
unmatchedInteractions: number;
|
||||
mismatchedEndpoints: number;
|
||||
}) => {
|
||||
return [
|
||||
...(unmatchedInteractions
|
||||
? [
|
||||
`🆕 ${unmatchedInteractions} undocumented path${
|
||||
unmatchedInteractions > 1 ? 's' : ''
|
||||
}`,
|
||||
]
|
||||
: []),
|
||||
...(mismatchedEndpoints
|
||||
? [
|
||||
`⚠️ ${mismatchedEndpoints} mismatch${
|
||||
mismatchedEndpoints > 1 ? 'es' : ''
|
||||
}`,
|
||||
]
|
||||
: []),
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
export const generateCompareSummaryMarkdown = (
|
||||
commit: { sha: string },
|
||||
results: CiRunDetails,
|
||||
options: { verbose: boolean },
|
||||
) => {
|
||||
const anyCompletedHasWarning = results.completed.some(
|
||||
s => s.warnings.length > 0,
|
||||
);
|
||||
const anyCompletedHasCapture = results.completed.some(s => s.capture);
|
||||
return `
|
||||
${
|
||||
results.completed.length > 0
|
||||
? `### APIs Changed
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<th>Changes</th>
|
||||
<th>Rules</th>
|
||||
${anyCompletedHasWarning ? '<th>Warnings</th>' : ''}
|
||||
${anyCompletedHasCapture ? '<th>Tests</th>' : ''}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.completed
|
||||
.map(
|
||||
s =>
|
||||
`<tr>
|
||||
<td>
|
||||
|
||||
${relative(cliPaths.targetDir, s.apiName)}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
${getOperationsText(s.comparison.groupedDiffs, {
|
||||
webUrl: s.opticWebUrl,
|
||||
verbose: options.verbose,
|
||||
labelJoiner: ',\n',
|
||||
})}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
${getChecksLabel(s.comparison.results, results.severity)}
|
||||
|
||||
</td>
|
||||
|
||||
${anyCompletedHasWarning ? `<td>${s.warnings.join('\n')}</td>` : ''}
|
||||
|
||||
${
|
||||
anyCompletedHasCapture
|
||||
? `
|
||||
|
||||
<td>
|
||||
|
||||
${
|
||||
s.capture
|
||||
? s.capture.success
|
||||
? s.capture.mismatchedEndpoints || s.capture.unmatchedInteractions
|
||||
? getCaptureIssuesLabel({
|
||||
unmatchedInteractions: s.capture.unmatchedInteractions,
|
||||
mismatchedEndpoints: s.capture.mismatchedEndpoints,
|
||||
})
|
||||
: `✅ ${s.capture.percentCovered}% coverage`
|
||||
: '❌ Failed to run'
|
||||
: ''
|
||||
}
|
||||
|
||||
</td>
|
||||
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</tr>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
results.failed.length > 0
|
||||
? `### Errors running optic
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.failed
|
||||
.map(
|
||||
s => `<tr>
|
||||
<td>${s.apiName}</td>
|
||||
<td>
|
||||
|
||||
${'```'}
|
||||
${s.error}
|
||||
${'```'}
|
||||
|
||||
</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
Summary of API changes for commit (${commit.sha})
|
||||
|
||||
${
|
||||
results.noop.length > 0
|
||||
? `${
|
||||
results.noop.length === 1 ? '1 API' : `${results.noop.length} APIs`
|
||||
} had no changes.`
|
||||
: ''
|
||||
}`;
|
||||
};
|
||||
@@ -42,6 +42,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"check:api": "backstage-repo-tools package schema openapi check",
|
||||
"clean": "backstage-cli package clean",
|
||||
"fuzz": "backstage-repo-tools package schema openapi fuzz --exclude-checks response_schema_conformance",
|
||||
"generate": "backstage-repo-tools package schema openapi generate --server --client-package packages/catalog-client",
|
||||
|
||||
@@ -10108,6 +10108,7 @@ __metadata:
|
||||
"@types/is-glob": ^4.0.2
|
||||
"@types/node": ^18.17.8
|
||||
"@types/prettier": ^2.0.0
|
||||
"@useoptic/openapi-utilities": ^0.54.8
|
||||
chalk: ^4.0.0
|
||||
codeowners-utils: ^1.0.2
|
||||
command-exists: ^1.2.9
|
||||
@@ -20292,6 +20293,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/json-pointer-helpers@npm:0.54.8":
|
||||
version: 0.54.8
|
||||
resolution: "@useoptic/json-pointer-helpers@npm:0.54.8"
|
||||
dependencies:
|
||||
jsonpointer: ^5.0.1
|
||||
minimatch: 9.0.3
|
||||
checksum: 4eddabb6dce3ca8160dcd4904299b6964945c3fe47d39bfeca6c68b9a50b058b901a6fb10ab168295475d651df3349149faa5f27f77293e15b6eee8d4417432e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/openapi-io@npm:0.50.10":
|
||||
version: 0.50.10
|
||||
resolution: "@useoptic/openapi-io@npm:0.50.10"
|
||||
@@ -20346,6 +20357,31 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/openapi-utilities@npm:^0.54.8":
|
||||
version: 0.54.8
|
||||
resolution: "@useoptic/openapi-utilities@npm:0.54.8"
|
||||
dependencies:
|
||||
"@useoptic/json-pointer-helpers": 0.54.8
|
||||
ajv: ^8.6.0
|
||||
ajv-errors: ~3.0.0
|
||||
ajv-formats: ~2.1.0
|
||||
chalk: ^4.1.2
|
||||
fast-deep-equal: ^3.1.3
|
||||
is-url: ^1.2.4
|
||||
js-yaml: ^4.1.0
|
||||
json-stable-stringify: ^1.0.1
|
||||
lodash.groupby: ^4.6.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.omit: ^4.5.0
|
||||
node-machine-id: ^1.1.12
|
||||
openapi-types: ^12.0.2
|
||||
ts-invariant: ^0.9.3
|
||||
url-join: ^4.0.1
|
||||
yaml-ast-parser: ^0.0.43
|
||||
checksum: fa9e9f430c77687591aaf8b43b7b31a7c2f80fe9c140aaa978f1948f84d3e974181c91c3d8ec3e06efca9735c7826290baf4be72063bf733887aa632b40c3c4a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/optic@npm:^0.50.10":
|
||||
version: 0.50.10
|
||||
resolution: "@useoptic/optic@npm:0.50.10"
|
||||
|
||||
Reference in New Issue
Block a user