Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Leverage autorest.bicep for schema generation #3042

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/generate-schemas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: recursive

- name: Clone azure-rest-api-specs
uses: actions/checkout@v4
Expand All @@ -41,6 +43,18 @@ jobs:
run: npm ci
working-directory: generator

- name: Build bicep-types
run: |
npm ci
npm run build
working-directory: bicep-types-az/bicep-types/src/bicep-types

- name: Build autorest.bicep
run: |
npm ci
npm run build
working-directory: bicep-types-az/src/autorest.bicep

- name: Run generator
run: |
rm -Rf "$GITHUB_WORKSPACE/schemas"
Expand Down
37 changes: 22 additions & 15 deletions .github/workflows/generate-single.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ on:
jobs:
update-schemas:
name: Update Schemas
permissions:
contents: write
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: recursive

- name: Clone azure-rest-api-specs
uses: actions/checkout@v4
Expand All @@ -36,26 +40,29 @@ jobs:
run: npm ci
working-directory: generator

- name: Build bicep-types
run: |
npm ci
npm run build
working-directory: bicep-types-az/bicep-types/src/bicep-types

- name: Build autorest.bicep
run: |
npm ci
npm run build
working-directory: bicep-types-az/src/autorest.bicep

- name: Run generator
run: |
npm run generate-single -- \
--specs-dir "$GITHUB_WORKSPACE/workflow-temp/azure-rest-api-specs" \
--base-path '${{ github.event.inputs.single_path }}'
working-directory: generator

- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
- name: Push to git branch
uses: stefanzweifel/git-auto-commit-action@v5
with:
committer: GitHub <[email protected]>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
signoff: false
branch: autogenerate-${{ github.event.inputs.single_path }}
delete-branch: true
title: |
Update Generated Schemas (${{ github.event.inputs.single_path }})
body: |
Update Generated Schemas (${{ github.event.inputs.single_path }})
commit-message: |
Update Generated Schemas (${{ github.event.inputs.single_path }})
labels: autogenerate
draft: false
commit_message: Update Generated Schemas (${{ github.event.inputs.single_path }})
branch: autogenerate-single/${{ github.event.inputs.single_path }}
push_options: '--force'
create_branch: true
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "bicep-types-az"]
path = bicep-types-az
url = https://github.com/Azure/bicep-types-az
1 change: 1 addition & 0 deletions bicep-types-az
Submodule bicep-types-az added at 4f20ac
3 changes: 1 addition & 2 deletions generator/autogenlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ const disabledProviders: AutoGenConfig[] = [
disabledForAutogen: true,
},
{
// Disabled until the unexpected character error in the swagger spec is fixed
basePath: 'cdn/resource-manager',
namespace: 'Microsoft.Cdn',
disabledForAutogen: true,
useAutorestV2: true,
},
{
// Disabled until the enum mismatch in the swagger spec is fixed
Expand Down
120 changes: 120 additions & 0 deletions generator/autorest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import path from 'path';
import os from 'os';
import { findRecursive, lowerCaseContains, executeCmd, fileExists } from './utils';
import * as constants from './constants';
import { ReadmeTag, AutoGenConfig, CodeBlock } from './models';
import * as cm from '@ts-common/commonmark-to-markdown'
import * as yaml from 'js-yaml'
import { readFile, writeFile } from 'fs/promises';

const autorestBinary = os.platform() === 'win32' ? 'autorest.cmd' : 'autorest';
export const apiVersionRegex = /^\d{4}-\d{2}-\d{2}(|-preview)$/;

async function execAutoRest(tmpFolder: string, params: string[]) {
await executeCmd(__dirname, `${__dirname}/node_modules/.bin/${autorestBinary}`, params);
if (!fileExists(tmpFolder)) {
return [];
}

return await findRecursive(tmpFolder, p => path.extname(p) === '.json');
}

export async function runAutorest(readme: string, tmpFolder: string) {
const autoRestParams = [
`--version=${constants.autorestCoreVersion}`,
`--use=@autorest/azureresourceschema@${constants.azureresourceschemaVersion}`,
'--azureresourceschema',
`--output-folder=${tmpFolder}`,
'--multiapi',
'--pass-thru:subset-reducer',
'--pass-thru:schema-validator-swagger',
readme,
];

if (constants.autoRestVerboseOutput) {
autoRestParams.push('--verbose');
}

return await execAutoRest(tmpFolder, autoRestParams);
}



export async function generateAutorestConfig(readme: string, autoGenConfig: AutoGenConfig) {
const content = (await readFile(readme)).toString();
const markdownEx = cm.parse(content);
const fileSet = new Set<string>();
for (const node of cm.iterate(markdownEx.markDown)) {
// We're only interested in yaml code blocks
if (node.type !== 'code_block' || !node.info || !node.literal ||
!node.info.trim().startsWith('yaml')) {
continue;
}

const DOC = (yaml.load(node.literal) as CodeBlock);
if (DOC) {
const inputFile = DOC['input-file'];
if (typeof inputFile === 'string') {
fileSet.add(inputFile);
} else if (inputFile instanceof Array) {
for (const i of inputFile) {
fileSet.add(i);
}
}
}
}

let readmeTag = {} as ReadmeTag;
for (const inputFile of fileSet) {
const pathComponents = inputFile.split("/");

if (!autoGenConfig.useNamespaceFromConfig &&
!lowerCaseContains(pathComponents, autoGenConfig.namespace)) {
continue;
}

const apiVersion = pathComponents.filter(p => p.match(apiVersionRegex) !== null)[0];
if (!apiVersion) {
continue;
}

readmeTag[apiVersion] ??= readmeTag[apiVersion] || [];
readmeTag[apiVersion].push(inputFile);
}

if (autoGenConfig.readmeTag) {
readmeTag = {...readmeTag, ...autoGenConfig.readmeTag };
}

const schemaReadmeContent = compositeSchemaReadme(readmeTag);

const schemaReadme = readme.replace(/\.md$/i, '.azureresourceschema.md');

await writeFile(schemaReadme, schemaReadmeContent);
}

function compositeSchemaReadme(readmeTag: ReadmeTag): string {
let content =
`## AzureResourceSchema

### AzureResourceSchema multi-api

\`\`\` yaml $(azureresourceschema) && $(multiapi)
${yaml.dump({ 'batch': Object.keys(readmeTag).map(apiVersion => ({ 'tag': `schema-${apiVersion}`})) }, { lineWidth: 1000 })}
\`\`\`

`
for (const apiVersion of Object.keys(readmeTag)) {
content +=
`
### Tag: schema-${apiVersion} and azureresourceschema

\`\`\` yaml $(tag) == 'schema-${apiVersion}' && $(azureresourceschema)
${yaml.dump({ 'input-file': readmeTag[apiVersion] }, { lineWidth: 1000})}
\`\`\`
`
}
return content;
}
127 changes: 127 additions & 0 deletions generator/autorestV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import path from 'path';
import os from 'os';
import { findRecursive, executeCmd, fileExists } from './utils';
import * as constants from './constants';
import { readFile, writeFile } from 'fs/promises';
import * as markdown from '@ts-common/commonmark-to-markdown'
import * as yaml from 'js-yaml'

const autorestBinary = os.platform() === 'win32' ? 'autorest.cmd' : 'autorest';

const rootDir = `${__dirname}/../`;
const extensionDir = path.resolve(`${rootDir}/bicep-types-az/src/autorest.bicep/`);

export async function generateAutorestV2Config(readmePath: string, bicepReadmePath: string) {
// We expect a path format convention of <provider>/(any/number/of/intervening/folders)/<yyyy>-<mm>-<dd>(|-preview)/<filename>.json
// This information is used to generate individual tags in the generated autorest configuration
// eslint-disable-next-line no-useless-escape
const pathRegex = /^(\$\(this-folder\)\/|)([^\/]+)(?:\/[^\/]+)*\/(\d{4}-\d{2}-\d{2}(|-preview))\/.*\.json$/i;

const readmeContents = await readFile(readmePath, { encoding: 'utf8' });
const readmeMarkdown = markdown.parse(readmeContents);

const inputFiles = new Set<string>();
// we need to look for all autorest configuration elements containing input files, and collect that list of files. These will look like (e.g.):
// ```yaml $(tag) == 'someTag'
// input-file:
// - path/to/file.json
// - path/to/other_file.json
// ```
for (const node of markdown.iterate(readmeMarkdown.markDown)) {
// We're only interested in yaml code blocks
if (node.type !== 'code_block' || !node.info || !node.literal ||
!node.info.trim().startsWith('yaml')) {
continue;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const yamlData = yaml.load(node.literal) as any;
if (yamlData) {
// input-file may be a single string or an array of strings
const inputFile = yamlData['input-file'];
if (typeof inputFile === 'string') {
inputFiles.add(inputFile);
} else if (inputFile instanceof Array) {
for (const i of inputFile) {
inputFiles.add(i);
}
}
}
}

const filesByTag: Record<string, string[]> = {};
for (const file of inputFiles) {
const normalizedFile = normalizeJsonPath(file);
const match = pathRegex.exec(normalizedFile);
if (match) {
// Generate a unique tag. We can't process all of the different API versions in one autorest pass
// because there are constraints on naming uniqueness (e.g. naming of definitions), so we want to pass over
// each API version separately.
const tagName = `${match[2].toLowerCase()}-${match[3].toLowerCase()}`;
if (!filesByTag[tagName]) {
filesByTag[tagName] = [];
}

filesByTag[tagName].push(normalizedFile);
} else {
console.warn(`WARNING: Unable to parse swagger path "${file}"`);
}
}

let generatedContent = `##Bicep

### Bicep multi-api
\`\`\`yaml $(bicep) && $(multiapi)
${yaml.dump({ 'batch': Object.keys(filesByTag).map(tag => ({ 'tag': tag })) }, { lineWidth: 1000 })}
\`\`\`
`;

for (const tag of Object.keys(filesByTag)) {
generatedContent += `### Tag: ${tag} and bicep
\`\`\`yaml $(tag) == '${tag}' && $(bicep)
${yaml.dump({ 'input-file': filesByTag[tag] }, { lineWidth: 1000})}
\`\`\`
`;
}

await writeFile(bicepReadmePath, generatedContent);
}

function normalizeJsonPath(jsonPath: string) {
// eslint-disable-next-line no-useless-escape
return path.normalize(jsonPath).replace(/[\\\/]/g, '/');
}

async function execAutoRest(tmpFolder: string, params: string[]) {
await executeCmd(__dirname, `${__dirname}/node_modules/.bin/${autorestBinary}`, params);
if (!fileExists(tmpFolder)) {
return [];
}

return await findRecursive(tmpFolder, p => path.extname(p) === '.json');
}

export async function runAutorestV2(readme: string, tmpFolder: string) {
const autoRestParams = [
`--use=@autorest/modelerfour`,
`--use=${extensionDir}`,
'--bicep',
`--output-folder=${tmpFolder}`,
'--multiapi',
'--title=none',
// This is necessary to avoid failures such as "ERROR: Semantic violation: Discriminator must be a required property." blocking type generation.
// In an ideal world, we'd raise issues in https://github.com/Azure/azure-rest-api-specs and force RP teams to fix them, but this isn't very practical
// as new validations are added continuously, and there's often quite a lag before teams will fix them - we don't want to be blocked by this in generating types.
`--skip-semantics-validation`,
`--arm-schema=true`,
readme,
];

if (constants.autoRestVerboseOutput) {
autoRestParams.push('--verbose');
}

return await execAutoRest(tmpFolder, autoRestParams);
}
Loading