Merge branch 'master' into feat/26838
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-github': patch
|
||||
---
|
||||
|
||||
Add `github:branch-protection:create` scaffolder action to set branch protection on an existing repository. Example usage:
|
||||
|
||||
```yaml
|
||||
- id: set-branch-protection
|
||||
name: Set Branch Protection
|
||||
action: github:branch-protection:create
|
||||
input:
|
||||
repoUrl: 'github.com?repo=backstage&owner=backstage'
|
||||
branch: master
|
||||
enforceAdmins: true # default
|
||||
requiredApprovingReviewCount: 1 # default
|
||||
requireBranchesToBeUpToDate: true # default
|
||||
requireCodeOwnerReviews: true
|
||||
dismissStaleReviews: true
|
||||
requiredConversationResolution: true
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-app-backend': patch
|
||||
---
|
||||
|
||||
Fixed unexpected behaviour where configuration supplied with `APP_CONFIG_*` environment variables where not filtered by the configuration schema.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': minor
|
||||
---
|
||||
|
||||
Adding negation keyword for entity filtering
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch
|
||||
---
|
||||
|
||||
Add `reviewers` input parameter to `publish:bitbucketServer:pull-request`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-azure': patch
|
||||
---
|
||||
|
||||
Updated dependency `azure-devops-node-api` to `^14.0.0`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Support `--max-warnings` flag for package linting
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Add tests for the `useTemplateDirectory` hook.
|
||||
@@ -185,11 +185,11 @@ Scope: The Backstage Documentation
|
||||
|
||||
## Sponsors
|
||||
|
||||
| Name | Organization | GitHub | Email |
|
||||
| ----------------- | ------------ | ------------------------------------------- | ------------------ |
|
||||
| Niklas Gustavsson | Spotify | [protocol7](https://github.com/protocol7) | ngn@spotify.com |
|
||||
| Dave Zolotusky | Spotify | [dzolotusky](https://github.com/dzolotusky) | dzolo@spotify.com |
|
||||
| Helen Greul | Spotify | [helengreul](https://github.com/helengreul) | heleng@spotify.com |
|
||||
| Name | Organization | GitHub | Email |
|
||||
| ----------------- | ------------ | ------------------------------------------- | ----------------- |
|
||||
| Niklas Gustavsson | Spotify | [protocol7](https://github.com/protocol7) | ngn@spotify.com |
|
||||
| Dave Zolotusky | Spotify | [dzolotusky](https://github.com/dzolotusky) | dzolo@spotify.com |
|
||||
| Pia Nilsson | Spotify | [pianilsson](https://github.com/pianilsson) | pia@spotify.com |
|
||||
|
||||
## Organization Members
|
||||
|
||||
@@ -224,9 +224,10 @@ Scope: The Backstage Documentation
|
||||
|
||||
## Emeritus End User Sponsors
|
||||
|
||||
| Name | Organization | GitHub | Discord |
|
||||
| --------- | ------------ | ------------------------------------------- | -------------- |
|
||||
| Lee Mills | Spotify | [leemills83](https://github.com/leemills83) | `.binarypoint` |
|
||||
| Name | Organization | GitHub | Discord |
|
||||
| ----------- | ------------ | ------------------------------------------- | -------------- |
|
||||
| Lee Mills | Spotify | [leemills83](https://github.com/leemills83) | `.binarypoint` |
|
||||
| Helen Greul | Spotify | [helengreul](https://github.com/helengreul) | `helen_greul` |
|
||||
|
||||
## Emeritus Project Area Maintainers
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"@backstage/plugin-techdocs-backend": "workspace:^",
|
||||
"@gitbeaker/node": "^35.1.0",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"azure-devops-node-api": "^12.0.0",
|
||||
"azure-devops-node-api": "^14.0.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"dockerode": "^4.0.0",
|
||||
"example-app": "link:../app",
|
||||
|
||||
@@ -237,6 +237,7 @@ Usage: backstage-cli package lint [options] [directories...]
|
||||
Options:
|
||||
--format <format>
|
||||
--fix
|
||||
--max-warnings <number>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
|
||||
@@ -156,6 +156,10 @@ export function registerScriptCommand(program: Command) {
|
||||
'eslint-formatter-friendly',
|
||||
)
|
||||
.option('--fix', 'Attempt to automatically fix violations')
|
||||
.option(
|
||||
'--max-warnings <number>',
|
||||
'Fail if more than this number of warnings (default: 0)',
|
||||
)
|
||||
.description('Lint a package')
|
||||
.action(lazy(() => import('./lint').then(m => m.default)));
|
||||
|
||||
|
||||
@@ -29,6 +29,13 @@ export default async (directories: string[], opts: OptionValues) => {
|
||||
directories.length ? directories : ['.'],
|
||||
);
|
||||
|
||||
const maxWarnings = opts.maxWarnings ?? 0;
|
||||
|
||||
const failed =
|
||||
results.some(r => r.errorCount > 0) ||
|
||||
results.reduce((current, next) => current + next.warningCount, 0) >
|
||||
maxWarnings;
|
||||
|
||||
if (opts.fix) {
|
||||
await ESLint.outputFixes(results);
|
||||
}
|
||||
@@ -39,12 +46,14 @@ export default async (directories: string[], opts: OptionValues) => {
|
||||
if (opts.format === 'eslint-formatter-friendly') {
|
||||
process.chdir(paths.targetRoot);
|
||||
}
|
||||
|
||||
const resultText = formatter.format(results);
|
||||
|
||||
// If there is any feedback at all, we treat it as a lint failure. This should be
|
||||
// consistent with our old behavior of passing `--max-warnings=0` when invoking eslint.
|
||||
if (resultText) {
|
||||
console.log(resultText);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,57 +27,7 @@ import {
|
||||
import { runParallelWorkers } from '../../lib/parallel';
|
||||
import { buildFrontend } from '../build/buildFrontend';
|
||||
import { buildBackend } from '../build/buildBackend';
|
||||
|
||||
function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) {
|
||||
// Regardless of what command instance is passed in we want to find
|
||||
// the root command and resolve the path from there
|
||||
let rootCmd = anyCmd;
|
||||
while (rootCmd.parent) {
|
||||
rootCmd = rootCmd.parent;
|
||||
}
|
||||
|
||||
// Now find the command that was requested
|
||||
let targetCmd = rootCmd as Command | undefined;
|
||||
for (const name of commandPath) {
|
||||
targetCmd = targetCmd?.commands.find(c => c.name() === name) as
|
||||
| Command
|
||||
| undefined;
|
||||
}
|
||||
|
||||
if (!targetCmd) {
|
||||
throw new Error(
|
||||
`Could not find package command '${commandPath.join(' ')}'`,
|
||||
);
|
||||
}
|
||||
const cmd = targetCmd;
|
||||
|
||||
const expectedScript = `backstage-cli ${commandPath.join(' ')}`;
|
||||
|
||||
return (scriptStr?: string) => {
|
||||
if (!scriptStr || !scriptStr.startsWith(expectedScript)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const argsStr = scriptStr.slice(expectedScript.length).trim();
|
||||
|
||||
// Can't clone or copy or even use commands as prototype, so we mutate
|
||||
// the necessary members instead, and then reset them once we're done
|
||||
const currentOpts = (cmd as any)._optionValues;
|
||||
const currentStore = (cmd as any)._storeOptionsAsProperties;
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
(cmd as any)._storeOptionsAsProperties = false;
|
||||
(cmd as any)._optionValues = result;
|
||||
|
||||
// Triggers the writing of options to the result object
|
||||
cmd.parseOptions(argsStr.split(' '));
|
||||
|
||||
(cmd as any)._storeOptionsAsProperties = currentOpts;
|
||||
(cmd as any)._optionValues = currentStore;
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
import { createScriptOptionsParser } from './optionsParser';
|
||||
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { OptionValues } from 'commander';
|
||||
import { Command, OptionValues } from 'commander';
|
||||
import { relative as relativePath } from 'path';
|
||||
import { PackageGraph, BackstagePackageJson } from '@backstage/cli-node';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { runWorkerQueueThreads } from '../../lib/parallel';
|
||||
import { createScriptOptionsParser } from './optionsParser';
|
||||
|
||||
function depCount(pkg: BackstagePackageJson) {
|
||||
const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0;
|
||||
@@ -29,7 +30,7 @@ function depCount(pkg: BackstagePackageJson) {
|
||||
return deps + devDeps;
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
if (opts.since) {
|
||||
@@ -54,22 +55,30 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
process.env.FORCE_COLOR = '1';
|
||||
}
|
||||
|
||||
const parseLintScript = createScriptOptionsParser(cmd, ['package', 'lint']);
|
||||
|
||||
const resultsList = await runWorkerQueueThreads({
|
||||
items: packages.map(pkg => ({
|
||||
fullDir: pkg.dir,
|
||||
relativeDir: relativePath(paths.targetRoot, pkg.dir),
|
||||
lintOptions: parseLintScript(pkg.packageJson.scripts?.lint),
|
||||
})),
|
||||
workerData: {
|
||||
fix: Boolean(opts.fix),
|
||||
format: opts.format as string | undefined,
|
||||
},
|
||||
workerFactory: async ({ fix, format }) => {
|
||||
const { ESLint } = require('eslint');
|
||||
const { ESLint } = require('eslint') as typeof import('eslint');
|
||||
|
||||
return async ({
|
||||
fullDir,
|
||||
relativeDir,
|
||||
}): Promise<{ relativeDir: string; resultText: string }> => {
|
||||
lintOptions,
|
||||
}): Promise<{
|
||||
relativeDir: string;
|
||||
resultText: string;
|
||||
failed: boolean;
|
||||
}> => {
|
||||
// Bit of a hack to make file resolutions happen from the correct directory
|
||||
// since some lint rules don't respect the cwd of ESLint
|
||||
process.cwd = () => fullDir;
|
||||
@@ -92,21 +101,34 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
await ESLint.outputFixes(results);
|
||||
}
|
||||
|
||||
const resultText = formatter.format(results);
|
||||
const maxWarnings = lintOptions?.maxWarnings ?? 0;
|
||||
const resultText = formatter.format(results) as string;
|
||||
const failed =
|
||||
results.some(r => r.errorCount > 0) ||
|
||||
results.reduce((current, next) => current + next.warningCount, 0) >
|
||||
maxWarnings;
|
||||
|
||||
return { relativeDir, resultText };
|
||||
return {
|
||||
relativeDir,
|
||||
resultText,
|
||||
failed,
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
let failed = false;
|
||||
for (const { relativeDir, resultText } of resultsList) {
|
||||
if (resultText) {
|
||||
console.log();
|
||||
console.log(chalk.red(`Lint failed in ${relativeDir}:`));
|
||||
console.log(resultText.trimStart());
|
||||
|
||||
for (const { relativeDir, resultText, failed: runFailed } of resultsList) {
|
||||
if (runFailed) {
|
||||
console.log(chalk.red(`Lint failed in ${relativeDir}`));
|
||||
failed = true;
|
||||
|
||||
// When doing repo lint, only list the results if the lint failed to avoid a log
|
||||
// dump of all warnings that might be irrelevant
|
||||
if (resultText) {
|
||||
console.log();
|
||||
console.log(resultText.trimStart());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { Command } from 'commander';
|
||||
|
||||
export function createScriptOptionsParser(
|
||||
anyCmd: Command,
|
||||
commandPath: string[],
|
||||
) {
|
||||
// Regardless of what command instance is passed in we want to find
|
||||
// the root command and resolve the path from there
|
||||
let rootCmd = anyCmd;
|
||||
while (rootCmd.parent) {
|
||||
rootCmd = rootCmd.parent;
|
||||
}
|
||||
|
||||
// Now find the command that was requested
|
||||
let targetCmd = rootCmd as Command | undefined;
|
||||
for (const name of commandPath) {
|
||||
targetCmd = targetCmd?.commands.find(c => c.name() === name) as
|
||||
| Command
|
||||
| undefined;
|
||||
}
|
||||
|
||||
if (!targetCmd) {
|
||||
throw new Error(
|
||||
`Could not find package command '${commandPath.join(' ')}'`,
|
||||
);
|
||||
}
|
||||
const cmd = targetCmd;
|
||||
|
||||
const expectedScript = `backstage-cli ${commandPath.join(' ')}`;
|
||||
|
||||
return (scriptStr?: string) => {
|
||||
if (!scriptStr || !scriptStr.startsWith(expectedScript)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const argsStr = scriptStr.slice(expectedScript.length).trim();
|
||||
|
||||
// Can't clone or copy or even use commands as prototype, so we mutate
|
||||
// the necessary members instead, and then reset them once we're done
|
||||
const currentOpts = (cmd as any)._optionValues;
|
||||
const currentStore = (cmd as any)._storeOptionsAsProperties;
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
(cmd as any)._storeOptionsAsProperties = false;
|
||||
(cmd as any)._optionValues = result;
|
||||
|
||||
// Triggers the writing of options to the result object
|
||||
cmd.parseOptions(argsStr.split(' '));
|
||||
|
||||
(cmd as any)._storeOptionsAsProperties = currentOpts;
|
||||
(cmd as any)._optionValues = currentStore;
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { readFrontendConfig } from './readFrontendConfig';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('readFrontendConfig', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockDir.clear();
|
||||
});
|
||||
|
||||
it('should validate env config', async () => {
|
||||
mockDir.setContent({
|
||||
'appDir/.config-schema.json': JSON.stringify({
|
||||
schemas: [
|
||||
{
|
||||
value: {
|
||||
type: 'object',
|
||||
|
||||
properties: {
|
||||
app: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
secretOfLife: {
|
||||
type: 'string',
|
||||
visibility: 'secret',
|
||||
},
|
||||
backendConfig: {
|
||||
type: 'string',
|
||||
visibility: 'backend',
|
||||
},
|
||||
publicValue: {
|
||||
type: 'string',
|
||||
visibility: 'frontend',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
backstageConfigSchemaVersion: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const config = new ConfigReader({
|
||||
app: {
|
||||
secretOfLife: '42',
|
||||
backendConfig: 'backend',
|
||||
publicValue: 'public',
|
||||
},
|
||||
});
|
||||
|
||||
const frontendConfig = await readFrontendConfig({
|
||||
env: {
|
||||
APP_CONFIG_app_secretOfLife: 'ignored',
|
||||
APP_CONFIG_app_backendConfig: 'ignored',
|
||||
APP_CONFIG_app_publicValue: 'injected',
|
||||
},
|
||||
appDistDir: `${mockDir.path}/appDir`,
|
||||
config,
|
||||
});
|
||||
|
||||
expect(frontendConfig).toEqual([
|
||||
{
|
||||
context: 'env',
|
||||
data: {
|
||||
app: {
|
||||
publicValue: 'injected',
|
||||
},
|
||||
},
|
||||
deprecatedKeys: [],
|
||||
filteredKeys: undefined,
|
||||
},
|
||||
{
|
||||
context: 'app',
|
||||
data: { app: { publicValue: 'public' } },
|
||||
deprecatedKeys: [],
|
||||
filteredKeys: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -36,10 +36,9 @@ export async function readFrontendConfig(options: {
|
||||
}): Promise<AppConfig[]> {
|
||||
const { env, appDistDir, config } = options;
|
||||
|
||||
const appConfigs = readEnvConfig(env);
|
||||
|
||||
const schemaPath = resolvePath(appDistDir, '.config-schema.json');
|
||||
if (await fs.pathExists(schemaPath)) {
|
||||
const envConfigs = readEnvConfig(env);
|
||||
const serializedSchema = await fs.readJson(schemaPath);
|
||||
|
||||
try {
|
||||
@@ -49,11 +48,10 @@ export async function readFrontendConfig(options: {
|
||||
serialized: serializedSchema,
|
||||
}));
|
||||
|
||||
const frontendConfigs = await schema.process(
|
||||
[{ data: config.get() as JsonObject, context: 'app' }],
|
||||
return await schema.process(
|
||||
[...envConfigs, { data: config.get() as JsonObject, context: 'app' }],
|
||||
{ visibility: ['frontend'], withDeprecatedKeys: true },
|
||||
);
|
||||
appConfigs.push(...frontendConfigs);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'Invalid app bundle schema. If this error is unexpected you need to run `yarn build` in the app. ' +
|
||||
@@ -63,5 +61,5 @@ export async function readFrontendConfig(options: {
|
||||
}
|
||||
}
|
||||
|
||||
return appConfigs;
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -93,6 +93,20 @@ describe('parseFilterExpression', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('recognizes negation key', () => {
|
||||
const component = { kind: 'Component' } as unknown as Entity;
|
||||
expect(run('not:kind:user')(component)).toBe(true);
|
||||
});
|
||||
|
||||
it('supports negation and affirmative expressions', () => {
|
||||
const component = {
|
||||
kind: 'Component',
|
||||
spec: { type: 'service' },
|
||||
} as unknown as Entity;
|
||||
expect(run('not:kind:user type:service')(component)).toBe(true);
|
||||
expect(run('type:service not:kind:user')(component)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects unknown keys', () => {
|
||||
expect(() => run('unknown:foo')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"'unknown' is not a valid filter expression key, expected one of 'kind','type','is','has'"`,
|
||||
@@ -123,17 +137,25 @@ describe('splitFilterExpression', () => {
|
||||
expect(run('')).toEqual([]);
|
||||
expect(run(' ')).toEqual([]);
|
||||
expect(run('kind:component')).toEqual([
|
||||
{ key: 'kind', parameters: ['component'] },
|
||||
{ key: 'kind', parameters: ['component'], negation: false },
|
||||
]);
|
||||
expect(run('kind:component,user')).toEqual([
|
||||
{ key: 'kind', parameters: ['component', 'user'] },
|
||||
{ key: 'kind', parameters: ['component', 'user'], negation: false },
|
||||
]);
|
||||
expect(run('kind:component,user not:type:foo')).toEqual([
|
||||
{ key: 'kind', parameters: ['component', 'user'], negation: false },
|
||||
{ key: 'type', parameters: ['foo'], negation: true },
|
||||
]);
|
||||
expect(run('not:type:foo kind:component,user')).toEqual([
|
||||
{ key: 'type', parameters: ['foo'], negation: true },
|
||||
{ key: 'kind', parameters: ['component', 'user'], negation: false },
|
||||
]);
|
||||
expect(run('kind:component,user type:foo')).toEqual([
|
||||
{ key: 'kind', parameters: ['component', 'user'] },
|
||||
{ key: 'type', parameters: ['foo'] },
|
||||
{ key: 'kind', parameters: ['component', 'user'], negation: false },
|
||||
{ key: 'type', parameters: ['foo'], negation: false },
|
||||
]);
|
||||
expect(run('with:multiple:colons')).toEqual([
|
||||
{ key: 'with', parameters: ['multiple:colons'] },
|
||||
{ key: 'with', parameters: ['multiple:colons'], negation: false },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const rootMatcherFactories: Record<
|
||||
(
|
||||
parameters: string[],
|
||||
onParseError: (error: Error) => void,
|
||||
negation?: boolean,
|
||||
) => EntityMatcherFn
|
||||
> = {
|
||||
kind: createKindMatcher,
|
||||
@@ -60,9 +61,9 @@ export function parseFilterExpression(expression: string): {
|
||||
const parts = splitFilterExpression(expression, e =>
|
||||
expressionParseErrors.push(e),
|
||||
);
|
||||
|
||||
const matchers = parts.flatMap(part => {
|
||||
const factory = rootMatcherFactories[part.key];
|
||||
const negation = part.negation;
|
||||
if (!factory) {
|
||||
const known = Object.keys(rootMatcherFactories).map(m => `'${m}'`);
|
||||
expressionParseErrors.push(
|
||||
@@ -76,7 +77,8 @@ export function parseFilterExpression(expression: string): {
|
||||
const matcher = factory(part.parameters, e =>
|
||||
expressionParseErrors.push(e),
|
||||
);
|
||||
return [matcher];
|
||||
|
||||
return [negation ? (entity: Entity) => !matcher(entity) : matcher];
|
||||
});
|
||||
|
||||
const filterFn = (entity: Entity) =>
|
||||
@@ -97,16 +99,20 @@ export function parseFilterExpression(expression: string): {
|
||||
export function splitFilterExpression(
|
||||
expression: string,
|
||||
onParseError: (error: Error) => void,
|
||||
): Array<{ key: string; parameters: string[] }> {
|
||||
): Array<{ key: string; parameters: string[]; negation: boolean }> {
|
||||
const words = expression
|
||||
.split(' ')
|
||||
.map(w => w.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const result = new Array<{ key: string; parameters: string[] }>();
|
||||
const result = new Array<{
|
||||
key: string;
|
||||
parameters: string[];
|
||||
negation: boolean;
|
||||
}>();
|
||||
|
||||
for (const word of words) {
|
||||
const match = word.match(/^([^:]+):(.+)$/);
|
||||
const match = word.match(/^(not:)?([^:]+):(.+)$/);
|
||||
if (!match) {
|
||||
onParseError(
|
||||
new InputError(
|
||||
@@ -115,11 +121,10 @@ export function splitFilterExpression(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = match[1];
|
||||
const parameters = match[2].split(',').filter(Boolean); // silently ignore double commas
|
||||
|
||||
result.push({ key, parameters });
|
||||
const key = match[2];
|
||||
const parameters = match[3].split(',').filter(Boolean); // silently ignore double commas
|
||||
const negation = Boolean(match[1]);
|
||||
result.push({ key, parameters, negation });
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^",
|
||||
"azure-devops-node-api": "^12.0.0",
|
||||
"azure-devops-node-api": "^14.0.0",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -44,6 +44,7 @@ export function createPublishBitbucketServerPullRequestAction(options: {
|
||||
description?: string | undefined;
|
||||
targetBranch?: string | undefined;
|
||||
sourceBranch: string;
|
||||
reviewers?: string[] | undefined;
|
||||
token?: string | undefined;
|
||||
gitAuthorName?: string | undefined;
|
||||
gitAuthorEmail?: string | undefined;
|
||||
|
||||
+6
-1
@@ -366,7 +366,7 @@ describe('publish:bitbucketServer:pull-request', () => {
|
||||
});
|
||||
|
||||
it(`should ${examples[4].description}`, async () => {
|
||||
expect.assertions(7);
|
||||
expect.assertions(8);
|
||||
server.use(
|
||||
rest.get(
|
||||
'https://no-credentials.bitbucket.com/rest/api/1.0/projects/project/repos/repo/branches',
|
||||
@@ -389,6 +389,7 @@ describe('publish:bitbucketServer:pull-request', () => {
|
||||
toRef: { displayId: string };
|
||||
fromRef: { displayId: string };
|
||||
description: string;
|
||||
reviewers: [{ user: { name: string } }];
|
||||
};
|
||||
expect(requestBody.title).toBe('My pull request');
|
||||
expect(requestBody.fromRef.displayId).toBe('my-feature-branch');
|
||||
@@ -396,6 +397,10 @@ describe('publish:bitbucketServer:pull-request', () => {
|
||||
expect(requestBody.description).toBe(
|
||||
'This is a detailed description of my pull request',
|
||||
);
|
||||
expect(requestBody.reviewers).toEqual([
|
||||
{ user: { name: 'reviewer1' } },
|
||||
{ user: { name: 'reviewer2' } },
|
||||
]);
|
||||
expect(req.headers.get('Authorization')).toBe(
|
||||
`Bearer ${yaml.parse(examples[4].example).steps[0].input.token}`,
|
||||
);
|
||||
|
||||
+1
@@ -107,6 +107,7 @@ export const examples: TemplateExample[] = [
|
||||
sourceBranch: 'my-feature-branch',
|
||||
targetBranch: 'development',
|
||||
description: 'This is a detailed description of my pull request',
|
||||
reviewers: ['reviewer1', 'reviewer2'],
|
||||
token: 'my-auth-token',
|
||||
gitAuthorName: 'test-user',
|
||||
gitAuthorEmail: 'test-user@sample.com',
|
||||
|
||||
+15
@@ -54,6 +54,7 @@ const createPullRequest = async (opts: {
|
||||
latestChangeset: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
reviewers?: string[];
|
||||
authorization: string;
|
||||
apiBaseUrl: string;
|
||||
}) => {
|
||||
@@ -64,6 +65,7 @@ const createPullRequest = async (opts: {
|
||||
description,
|
||||
toRef,
|
||||
fromRef,
|
||||
reviewers,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
} = opts;
|
||||
@@ -80,6 +82,7 @@ const createPullRequest = async (opts: {
|
||||
locked: true,
|
||||
toRef: toRef,
|
||||
fromRef: fromRef,
|
||||
reviewers: reviewers?.map(reviewer => ({ user: { name: reviewer } })),
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
@@ -257,6 +260,7 @@ export function createPublishBitbucketServerPullRequestAction(options: {
|
||||
description?: string;
|
||||
targetBranch?: string;
|
||||
sourceBranch: string;
|
||||
reviewers?: string[];
|
||||
token?: string;
|
||||
gitAuthorName?: string;
|
||||
gitAuthorEmail?: string;
|
||||
@@ -292,6 +296,15 @@ export function createPublishBitbucketServerPullRequestAction(options: {
|
||||
type: 'string',
|
||||
description: 'Branch of repository to copy changes from',
|
||||
},
|
||||
reviewers: {
|
||||
title: 'Pull Request Reviewers',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description:
|
||||
'The usernames of reviewers that will be added to the pull request',
|
||||
},
|
||||
token: {
|
||||
title: 'Authorization Token',
|
||||
type: 'string',
|
||||
@@ -327,6 +340,7 @@ export function createPublishBitbucketServerPullRequestAction(options: {
|
||||
description,
|
||||
targetBranch,
|
||||
sourceBranch,
|
||||
reviewers,
|
||||
gitAuthorName,
|
||||
gitAuthorEmail,
|
||||
} = ctx.input;
|
||||
@@ -480,6 +494,7 @@ export function createPublishBitbucketServerPullRequestAction(options: {
|
||||
description,
|
||||
toRef,
|
||||
fromRef,
|
||||
reviewers,
|
||||
authorization,
|
||||
apiBaseUrl,
|
||||
});
|
||||
|
||||
@@ -49,6 +49,41 @@ export function createGithubAutolinksAction(options: {
|
||||
JsonObject
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function createGithubBranchProtectionAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}): TemplateAction<
|
||||
{
|
||||
repoUrl: string;
|
||||
branch?: string | undefined;
|
||||
enforceAdmins?: boolean | undefined;
|
||||
requiredApprovingReviewCount?: number | undefined;
|
||||
requireCodeOwnerReviews?: boolean | undefined;
|
||||
dismissStaleReviews?: boolean | undefined;
|
||||
bypassPullRequestAllowances?:
|
||||
| {
|
||||
users?: string[];
|
||||
teams?: string[];
|
||||
apps?: string[];
|
||||
}
|
||||
| undefined;
|
||||
restrictions?:
|
||||
| {
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[];
|
||||
}
|
||||
| undefined;
|
||||
requiredStatusCheckContexts?: string[] | undefined;
|
||||
requireBranchesToBeUpToDate?: boolean | undefined;
|
||||
requiredConversationResolution?: boolean | undefined;
|
||||
requireLastPushApproval?: boolean | undefined;
|
||||
requiredCommitSigning?: boolean | undefined;
|
||||
token?: string | undefined;
|
||||
},
|
||||
JsonObject
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function createGithubDeployKeyAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import yaml from 'yaml';
|
||||
import { examples } from './githubBranchProtection.examples';
|
||||
import { createGithubBranchProtectionAction } from './githubBranchProtection';
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
repos: {
|
||||
createCommitSignatureProtection: jest.fn(),
|
||||
get: jest.fn(),
|
||||
updateBranchProtection: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
jest.mock('octokit', () => ({
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('github:branch-protection:create', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{ host: 'github.com', token: 'tokenlols' },
|
||||
{ host: 'ghe.github.com' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
let action: TemplateAction<any>;
|
||||
|
||||
const mockContext = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
mockOctokit.rest.repos.get.mockResolvedValue({
|
||||
data: {
|
||||
default_branch: 'master',
|
||||
},
|
||||
});
|
||||
|
||||
action = createGithubBranchProtectionAction({
|
||||
integrations,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create branch protection for the default branch with default params', async () => {
|
||||
const input = yaml.parse(examples[0].example).steps[0].input;
|
||||
const ctx = Object.assign({}, mockContext, { input });
|
||||
await action.handler(ctx);
|
||||
|
||||
expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({
|
||||
mediaType: {
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
branch: 'master',
|
||||
required_status_checks: {
|
||||
strict: true,
|
||||
contexts: [],
|
||||
},
|
||||
restrictions: null,
|
||||
enforce_admins: true,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: 1,
|
||||
require_code_owner_reviews: false,
|
||||
bypass_pull_request_allowances: undefined,
|
||||
dismiss_stale_reviews: false,
|
||||
require_last_push_approval: false,
|
||||
},
|
||||
required_conversation_resolution: false,
|
||||
});
|
||||
expect(
|
||||
mockOctokit.rest.repos.createCommitSignatureProtection,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create branch protection for the specified branch', async () => {
|
||||
const input = yaml.parse(examples[1].example).steps[0].input;
|
||||
const ctx = Object.assign({}, mockContext, { input });
|
||||
await action.handler(ctx);
|
||||
|
||||
expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({
|
||||
mediaType: {
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
branch: 'my-awesome-branch',
|
||||
required_status_checks: {
|
||||
strict: true,
|
||||
contexts: [],
|
||||
},
|
||||
restrictions: null,
|
||||
enforce_admins: true,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: 1,
|
||||
require_code_owner_reviews: false,
|
||||
bypass_pull_request_allowances: undefined,
|
||||
dismiss_stale_reviews: false,
|
||||
require_last_push_approval: false,
|
||||
},
|
||||
required_conversation_resolution: false,
|
||||
});
|
||||
expect(
|
||||
mockOctokit.rest.repos.createCommitSignatureProtection,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create branch protection with params and require commit signing', async () => {
|
||||
const input = yaml.parse(examples[2].example).steps[0].input;
|
||||
const ctx = Object.assign({}, mockContext, { input });
|
||||
await action.handler(ctx);
|
||||
|
||||
expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({
|
||||
mediaType: {
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
branch: 'master',
|
||||
required_status_checks: {
|
||||
strict: true,
|
||||
contexts: ['test'],
|
||||
},
|
||||
restrictions: null,
|
||||
enforce_admins: true,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: 1,
|
||||
require_code_owner_reviews: true,
|
||||
bypass_pull_request_allowances: undefined,
|
||||
dismiss_stale_reviews: true,
|
||||
require_last_push_approval: true,
|
||||
},
|
||||
required_conversation_resolution: true,
|
||||
});
|
||||
expect(
|
||||
mockOctokit.rest.repos.createCommitSignatureProtection,
|
||||
).toHaveBeenCalledWith({
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
branch: 'master',
|
||||
});
|
||||
});
|
||||
});
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { TemplateExample } from '@backstage/plugin-scaffolder-node';
|
||||
import yaml from 'yaml';
|
||||
|
||||
export const examples: TemplateExample[] = [
|
||||
{
|
||||
description: `GitHub Branch Protection for repository's default branch.`,
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
action: 'github:branch-protection:create',
|
||||
name: 'Setup Branch Protection',
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: `GitHub Branch Protection for a specific branch.`,
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
action: 'github:branch-protection:create',
|
||||
name: 'Setup Branch Protection',
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
branch: 'my-awesome-branch',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description: `GitHub Branch Protection and required commit signing on default branch.`,
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
action: 'github:branch-protection:create',
|
||||
name: 'Setup Branch Protection',
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
requireCodeOwnerReviews: true,
|
||||
requiredStatusCheckContexts: ['test'],
|
||||
dismissStaleReviews: true,
|
||||
requireLastPushApproval: true,
|
||||
requiredConversationResolution: true,
|
||||
requiredCommitSigning: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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 { createGithubBranchProtectionAction } from './githubBranchProtection';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
repos: {
|
||||
createCommitSignatureProtection: jest.fn(),
|
||||
get: jest.fn(),
|
||||
updateBranchProtection: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('octokit', () => ({
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('github:branch-protection:create', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{ host: 'github.com', token: 'tokenlols' },
|
||||
{ host: 'ghe.github.com' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
let action: TemplateAction<any>;
|
||||
|
||||
const mockContext = createMockActionContext({
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repository&owner=owner',
|
||||
name: 'envname',
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockOctokit.rest.repos.get.mockResolvedValue({
|
||||
data: {
|
||||
default_branch: 'master',
|
||||
},
|
||||
});
|
||||
|
||||
action = createGithubBranchProtectionAction({
|
||||
integrations,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
||||
it('should work with default params', async () => {
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({
|
||||
mediaType: {
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner: 'owner',
|
||||
repo: 'repository',
|
||||
branch: 'master',
|
||||
required_status_checks: {
|
||||
strict: true,
|
||||
contexts: [],
|
||||
},
|
||||
restrictions: null,
|
||||
enforce_admins: true,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: 1,
|
||||
require_code_owner_reviews: false,
|
||||
bypass_pull_request_allowances: undefined,
|
||||
dismiss_stale_reviews: false,
|
||||
require_last_push_approval: false,
|
||||
},
|
||||
required_conversation_resolution: false,
|
||||
});
|
||||
expect(
|
||||
mockOctokit.rest.repos.createCommitSignatureProtection,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require commit signing on default branch', async () => {
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
requiredCommitSigning: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({
|
||||
mediaType: {
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner: 'owner',
|
||||
repo: 'repository',
|
||||
branch: 'master',
|
||||
required_status_checks: {
|
||||
strict: true,
|
||||
contexts: [],
|
||||
},
|
||||
restrictions: null,
|
||||
enforce_admins: true,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: 1,
|
||||
require_code_owner_reviews: false,
|
||||
bypass_pull_request_allowances: undefined,
|
||||
dismiss_stale_reviews: false,
|
||||
require_last_push_approval: false,
|
||||
},
|
||||
required_conversation_resolution: false,
|
||||
});
|
||||
expect(
|
||||
mockOctokit.rest.repos.createCommitSignatureProtection,
|
||||
).toHaveBeenCalledWith({
|
||||
owner: 'owner',
|
||||
repo: 'repository',
|
||||
branch: 'master',
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with all params supplied', async () => {
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
branch: 'branch',
|
||||
enforceAdmins: false,
|
||||
requiredApprovingReviewCount: 2,
|
||||
requireCodeOwnerReviews: true,
|
||||
dismissStaleReviews: true,
|
||||
bypassPullRequestAllowances: {
|
||||
users: ['user1'],
|
||||
teams: ['team1'],
|
||||
apps: ['app1'],
|
||||
},
|
||||
restrictions: {
|
||||
users: ['user2'],
|
||||
teams: ['team2'],
|
||||
apps: ['app2'],
|
||||
},
|
||||
requiredStatusCheckContexts: ['context1', 'context2'],
|
||||
requireBranchesToBeUpToDate: false,
|
||||
requiredConversationResolution: true,
|
||||
requireLastPushApproval: true,
|
||||
requiredCommitSigning: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockOctokit.rest.repos.updateBranchProtection).toHaveBeenCalledWith({
|
||||
mediaType: {
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner: 'owner',
|
||||
repo: 'repository',
|
||||
branch: 'branch',
|
||||
required_status_checks: {
|
||||
strict: false,
|
||||
contexts: ['context1', 'context2'],
|
||||
},
|
||||
restrictions: {
|
||||
users: ['user2'],
|
||||
teams: ['team2'],
|
||||
apps: ['app2'],
|
||||
},
|
||||
enforce_admins: false,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: 2,
|
||||
require_code_owner_reviews: true,
|
||||
bypass_pull_request_allowances: {
|
||||
users: ['user1'],
|
||||
teams: ['team1'],
|
||||
apps: ['app1'],
|
||||
},
|
||||
dismiss_stale_reviews: true,
|
||||
require_last_push_approval: true,
|
||||
},
|
||||
required_conversation_resolution: true,
|
||||
});
|
||||
expect(
|
||||
mockOctokit.rest.repos.createCommitSignatureProtection,
|
||||
).toHaveBeenCalledWith({
|
||||
owner: 'owner',
|
||||
repo: 'repository',
|
||||
branch: 'branch',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 { InputError } from '@backstage/errors';
|
||||
import {
|
||||
createTemplateAction,
|
||||
parseRepoUrl,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { examples } from './githubBranchProtection.examples';
|
||||
import * as inputProps from './inputProperties';
|
||||
import { getOctokitOptions } from './helpers';
|
||||
import { Octokit } from 'octokit';
|
||||
import { enableBranchProtectionOnDefaultRepoBranch } from './gitHelpers';
|
||||
|
||||
/**
|
||||
* Creates an `github:branch-protection:create` Scaffolder action that configured Branch Protection in a Github Repository.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createGithubBranchProtectionAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) {
|
||||
const { integrations } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
repoUrl: string;
|
||||
branch?: string;
|
||||
enforceAdmins?: boolean;
|
||||
requiredApprovingReviewCount?: number;
|
||||
requireCodeOwnerReviews?: boolean;
|
||||
dismissStaleReviews?: boolean;
|
||||
bypassPullRequestAllowances?:
|
||||
| {
|
||||
users?: string[];
|
||||
teams?: string[];
|
||||
apps?: string[];
|
||||
}
|
||||
| undefined;
|
||||
restrictions?:
|
||||
| {
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[];
|
||||
}
|
||||
| undefined;
|
||||
requiredStatusCheckContexts?: string[];
|
||||
requireBranchesToBeUpToDate?: boolean;
|
||||
requiredConversationResolution?: boolean;
|
||||
requireLastPushApproval?: boolean;
|
||||
requiredCommitSigning?: boolean;
|
||||
token?: string;
|
||||
}>({
|
||||
id: 'github:branch-protection:create',
|
||||
description: 'Configures Branch Protection',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['repoUrl'],
|
||||
properties: {
|
||||
repoUrl: inputProps.repoUrl,
|
||||
branch: {
|
||||
title: 'Branch name',
|
||||
description: `The branch to protect. Defaults to the repository's default branch`,
|
||||
type: 'string',
|
||||
},
|
||||
enforceAdmins: inputProps.protectEnforceAdmins,
|
||||
requiredApprovingReviewCount: inputProps.requiredApprovingReviewCount,
|
||||
requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews,
|
||||
dismissStaleReviews: inputProps.dismissStaleReviews,
|
||||
bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances,
|
||||
restrictions: inputProps.restrictions,
|
||||
requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts,
|
||||
requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate,
|
||||
requiredConversationResolution:
|
||||
inputProps.requiredConversationResolution,
|
||||
requireLastPushApproval: inputProps.requireLastPushApproval,
|
||||
requiredCommitSigning: inputProps.requiredCommitSigning,
|
||||
token: inputProps.token,
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const {
|
||||
repoUrl,
|
||||
branch,
|
||||
enforceAdmins = true,
|
||||
requiredApprovingReviewCount = 1,
|
||||
requireCodeOwnerReviews = false,
|
||||
dismissStaleReviews = false,
|
||||
bypassPullRequestAllowances,
|
||||
restrictions,
|
||||
requiredStatusCheckContexts = [],
|
||||
requireBranchesToBeUpToDate = true,
|
||||
requiredConversationResolution = false,
|
||||
requireLastPushApproval = false,
|
||||
requiredCommitSigning = false,
|
||||
token: providedToken,
|
||||
} = ctx.input;
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError(`No owner provided for repo ${repoUrl}`);
|
||||
}
|
||||
|
||||
const repository = await client.rest.repos.get({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
});
|
||||
|
||||
await enableBranchProtectionOnDefaultRepoBranch({
|
||||
repoName: repo,
|
||||
client,
|
||||
owner,
|
||||
logger: ctx.logger,
|
||||
requireCodeOwnerReviews,
|
||||
bypassPullRequestAllowances,
|
||||
requiredApprovingReviewCount,
|
||||
restrictions,
|
||||
requiredStatusCheckContexts,
|
||||
requireBranchesToBeUpToDate,
|
||||
requiredConversationResolution,
|
||||
requireLastPushApproval,
|
||||
defaultBranch: branch ?? repository.data.default_branch,
|
||||
enforceAdmins,
|
||||
dismissStaleReviews,
|
||||
requiredCommitSigning,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -28,5 +28,6 @@ export {
|
||||
export { createPublishGithubAction } from './github';
|
||||
export { createGithubAutolinksAction } from './githubAutolinks';
|
||||
export { createGithubPagesEnableAction } from './githubPagesEnable';
|
||||
export { createGithubBranchProtectionAction } from './githubBranchProtection';
|
||||
|
||||
export { getOctokitOptions } from './helpers';
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
createPublishGithubAction,
|
||||
createPublishGithubPullRequestAction,
|
||||
createGithubPagesEnableAction,
|
||||
createGithubBranchProtectionAction,
|
||||
} from './actions';
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
@@ -102,6 +103,9 @@ export const githubModule = createBackendModule({
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
createGithubBranchProtectionAction({
|
||||
integrations,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '../../../routes';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import { WebFileSystemStore } from '../../../lib/filesystem/WebFileSystemAccess';
|
||||
import { WebFileSystemStore } from '../../../lib/filesystem/WebFileSystemStore';
|
||||
import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate';
|
||||
|
||||
export function TemplateIntroPage() {
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { useTemplateDirectory } from './useTemplateDirectory';
|
||||
import {
|
||||
createExampleTemplate,
|
||||
TemplateDirectoryAccess,
|
||||
WebFileSystemStore,
|
||||
} from '../../../lib/filesystem';
|
||||
import {
|
||||
IterableDirectoryHandle,
|
||||
WebFileSystemAccess,
|
||||
} from '../../../lib/filesystem/WebFileSystemAccess';
|
||||
|
||||
jest.mock('../../../lib/filesystem/createExampleTemplate');
|
||||
|
||||
describe('useTemplateDirectory', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return undefined when there is no existing directory in the file system store', async () => {
|
||||
jest.spyOn(WebFileSystemStore, 'getDirectory').mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useTemplateDirectory());
|
||||
|
||||
expect(result.current.directory).toBeUndefined();
|
||||
expect(result.current.loading).toBeTruthy();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBeFalsy());
|
||||
|
||||
expect(result.current.directory).toBeUndefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return an access when there is existing directory in the file system store', async () => {
|
||||
const handle = {} as IterableDirectoryHandle;
|
||||
|
||||
jest.spyOn(WebFileSystemStore, 'getDirectory').mockResolvedValue(handle);
|
||||
|
||||
const { result } = renderHook(() => useTemplateDirectory());
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBeFalsy());
|
||||
|
||||
expect(result.current.directory).toMatchObject({ handle });
|
||||
expect(result.current.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle opening a directory', async () => {
|
||||
const handle = {};
|
||||
jest
|
||||
.spyOn(WebFileSystemStore, 'getDirectory')
|
||||
.mockResolvedValue(handle as IterableDirectoryHandle);
|
||||
const setDirectory = jest
|
||||
.spyOn(WebFileSystemStore, 'setDirectory')
|
||||
.mockResolvedValue(undefined);
|
||||
const requestDirectoryAccess = jest
|
||||
.spyOn(WebFileSystemAccess, 'requestDirectoryAccess')
|
||||
.mockResolvedValue(handle as TemplateDirectoryAccess);
|
||||
|
||||
const { result } = renderHook(() => useTemplateDirectory());
|
||||
|
||||
expect(result.current.directory).toBeUndefined();
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleOpenDirectory();
|
||||
});
|
||||
|
||||
expect(requestDirectoryAccess).toHaveBeenCalled();
|
||||
expect(setDirectory).toHaveBeenCalledWith(handle);
|
||||
|
||||
await waitFor(() => expect(result.current.directory).toBeDefined());
|
||||
});
|
||||
|
||||
it('should handle creating a directory', async () => {
|
||||
const handle = {};
|
||||
(createExampleTemplate as jest.Mock).mockResolvedValue(handle);
|
||||
jest
|
||||
.spyOn(WebFileSystemStore, 'getDirectory')
|
||||
.mockResolvedValue(handle as IterableDirectoryHandle);
|
||||
const setDirectory = jest
|
||||
.spyOn(WebFileSystemStore, 'setDirectory')
|
||||
.mockResolvedValue(undefined);
|
||||
const requestDirectoryAccess = jest
|
||||
.spyOn(WebFileSystemAccess, 'requestDirectoryAccess')
|
||||
.mockResolvedValue(handle as TemplateDirectoryAccess);
|
||||
|
||||
const { result } = renderHook(() => useTemplateDirectory());
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleCreateDirectory();
|
||||
});
|
||||
|
||||
expect(requestDirectoryAccess).toHaveBeenCalled();
|
||||
expect(setDirectory).toHaveBeenCalledWith(handle);
|
||||
expect(createExampleTemplate).toHaveBeenCalledWith(handle);
|
||||
});
|
||||
|
||||
it('should handle closing a directory', async () => {
|
||||
jest.spyOn(WebFileSystemStore, 'getDirectory').mockResolvedValue(undefined);
|
||||
|
||||
const setDirectory = jest
|
||||
.spyOn(WebFileSystemStore, 'setDirectory')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useTemplateDirectory());
|
||||
|
||||
expect(setDirectory).not.toHaveBeenCalled();
|
||||
expect(result.current.directory).toBeUndefined();
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleCloseDirectory();
|
||||
});
|
||||
|
||||
expect(setDirectory).toHaveBeenCalledWith(undefined);
|
||||
await waitFor(() => expect(result.current.directory).toBeUndefined());
|
||||
});
|
||||
});
|
||||
@@ -18,11 +18,11 @@ import { useCallback } from 'react';
|
||||
import useAsyncRetry from 'react-use/esm/useAsyncRetry';
|
||||
|
||||
import {
|
||||
WebFileSystemAccess,
|
||||
WebFileSystemStore,
|
||||
WebFileSystemAccess,
|
||||
WebDirectoryAccess,
|
||||
createExampleTemplate,
|
||||
} from '../../../lib/filesystem';
|
||||
import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate';
|
||||
|
||||
export function useTemplateDirectory(): {
|
||||
directory?: WebDirectoryAccess;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { get, set } from 'idb-keyval';
|
||||
import { TemplateDirectoryAccess, TemplateFileAccess } from './types';
|
||||
|
||||
type WritableFileHandle = FileSystemFileHandle & {
|
||||
@@ -25,7 +24,7 @@ type WritableFileHandle = FileSystemFileHandle & {
|
||||
};
|
||||
|
||||
// A nicer type than the one from the TS lib
|
||||
interface IterableDirectoryHandle extends FileSystemDirectoryHandle {
|
||||
export interface IterableDirectoryHandle extends FileSystemDirectoryHandle {
|
||||
values(): AsyncIterable<
|
||||
| ({ kind: 'file' } & WritableFileHandle)
|
||||
| ({ kind: 'directory' } & IterableDirectoryHandle)
|
||||
@@ -124,16 +123,3 @@ export class WebFileSystemAccess {
|
||||
|
||||
private constructor() {}
|
||||
}
|
||||
|
||||
export class WebFileSystemStore {
|
||||
private static readonly key = 'scalfolder-template-editor-directory';
|
||||
|
||||
static async getDirectory(): Promise<IterableDirectoryHandle | undefined> {
|
||||
const directory = await get(WebFileSystemStore.key);
|
||||
return directory.handle;
|
||||
}
|
||||
|
||||
static async setDirectory(directory: TemplateDirectoryAccess | undefined) {
|
||||
return set(WebFileSystemStore.key, directory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { get, set } from 'idb-keyval';
|
||||
import { TemplateDirectoryAccess } from './types';
|
||||
import { IterableDirectoryHandle } from './WebFileSystemAccess';
|
||||
|
||||
export class WebFileSystemStore {
|
||||
private static readonly key = 'scalfolder-template-editor-directory';
|
||||
|
||||
static async getDirectory(): Promise<IterableDirectoryHandle | undefined> {
|
||||
const directory = await get(WebFileSystemStore.key);
|
||||
return directory.handle;
|
||||
}
|
||||
|
||||
static async setDirectory(directory: TemplateDirectoryAccess | undefined) {
|
||||
return set(WebFileSystemStore.key, directory);
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { TemplateFileAccess, TemplateDirectoryAccess } from './types';
|
||||
export { blobToBase64 } from './helpers';
|
||||
export {
|
||||
WebDirectoryAccess,
|
||||
WebFileSystemAccess,
|
||||
WebFileSystemStore,
|
||||
} from './WebFileSystemAccess';
|
||||
export { createExampleTemplate } from './createExampleTemplate';
|
||||
export type { TemplateFileAccess, TemplateDirectoryAccess } from './types';
|
||||
export { WebFileSystemStore } from './WebFileSystemStore';
|
||||
export { WebDirectoryAccess, WebFileSystemAccess } from './WebFileSystemAccess';
|
||||
|
||||
@@ -7200,7 +7200,7 @@ __metadata:
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
|
||||
azure-devops-node-api: ^12.0.0
|
||||
azure-devops-node-api: ^14.0.0
|
||||
yaml: ^2.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -8756,8 +8756,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@changesets/cli@npm:^2.14.0":
|
||||
version: 2.27.8
|
||||
resolution: "@changesets/cli@npm:2.27.8"
|
||||
version: 2.27.9
|
||||
resolution: "@changesets/cli@npm:2.27.9"
|
||||
dependencies:
|
||||
"@changesets/apply-release-plan": ^7.0.5
|
||||
"@changesets/assemble-release-plan": ^6.0.4
|
||||
@@ -8774,14 +8774,12 @@ __metadata:
|
||||
"@changesets/types": ^6.0.0
|
||||
"@changesets/write": ^0.3.2
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
"@types/semver": ^7.5.0
|
||||
ansi-colors: ^4.1.3
|
||||
ci-info: ^3.7.0
|
||||
enquirer: ^2.3.0
|
||||
external-editor: ^3.1.0
|
||||
fs-extra: ^7.0.1
|
||||
mri: ^1.2.0
|
||||
outdent: ^0.5.0
|
||||
p-limit: ^2.2.0
|
||||
package-manager-detector: ^0.2.0
|
||||
picocolors: ^1.1.0
|
||||
@@ -8791,7 +8789,7 @@ __metadata:
|
||||
term-size: ^2.1.0
|
||||
bin:
|
||||
changeset: bin.js
|
||||
checksum: b58386716b337976d5797debd4a418fb257bec1e6c9932b99eac7725dd5a76fceff32691c625f897fd4baa78aa2bfba9747fbacf7d8193197f9246b36d31c013
|
||||
checksum: 4bd36c152f9f93716b001f3ed849717588d2a9eb97f058e86f95ba6a43d8e4311073174251150aabb96f0a1ab5f8ab5ee6a32f85fc9248363f92b3826227cb9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18074,9 +18072,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash@npm:^4.14.151":
|
||||
version: 4.17.9
|
||||
resolution: "@types/lodash@npm:4.17.9"
|
||||
checksum: 6d1bf3e77f0a54d97532755a74260d402d8972259c5451b74612c16cb983b73e0760e5bfe4f9e68ab15051511c867812b40715a01f9805afe6bc36c7dd676378
|
||||
version: 4.17.10
|
||||
resolution: "@types/lodash@npm:4.17.10"
|
||||
checksum: 4600f2f25270c8fee6953e363d318149a5f0f1b1bb820aa2f42d7ada6e4f7de31848bb5ffc2c687b40bd73aa982167bdd6e6d8d456e72abe0c660ec77d1fa7e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21546,13 +21544,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"azure-devops-node-api@npm:^12.0.0":
|
||||
version: 12.5.0
|
||||
resolution: "azure-devops-node-api@npm:12.5.0"
|
||||
"azure-devops-node-api@npm:^14.0.0":
|
||||
version: 14.0.2
|
||||
resolution: "azure-devops-node-api@npm:14.0.2"
|
||||
dependencies:
|
||||
tunnel: 0.0.6
|
||||
typed-rest-client: ^1.8.4
|
||||
checksum: 7c2c3ae21eaf1bc3627ba4ea87bdac1085a3594eacf40eb6d7b11292f057988db38f718f4597733c6861d854c28bfe146bcf3964a13adddebe1085270bb63097
|
||||
typed-rest-client: ^2.0.1
|
||||
checksum: 26f39c7772d313befd49696fe2a1e91a8ffd75cfeb0c7f305a7102867f1090d299544361663b14e99bbebbf5855d1fc56ead0e4e32ee53bc83b487cdbe30d254
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -24972,13 +24970,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"des.js@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "des.js@npm:1.0.1"
|
||||
"des.js@npm:^1.0.0, des.js@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "des.js@npm:1.1.0"
|
||||
dependencies:
|
||||
inherits: ^2.0.1
|
||||
minimalistic-assert: ^1.0.0
|
||||
checksum: 1ec2eedd7ed6bd61dd5e0519fd4c96124e93bb22de8a9d211b02d63e5dd152824853d919bb2090f965cc0e3eb9c515950a9836b332020d810f9c71feb0fd7df4
|
||||
checksum: 0e9c1584b70d31e20f20a613fc9ef60fbc6a147dfec9e448a168794a4b97ac04d8dc47ea008f1fa93b0f8aaf7c1ead632a5e59ce1913a6079d2d244c9f5ebe33
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -26929,7 +26927,7 @@ __metadata:
|
||||
"@types/express": ^4.17.6
|
||||
"@types/express-serve-static-core": ^4.17.5
|
||||
"@types/luxon": ^3.0.0
|
||||
azure-devops-node-api: ^12.0.0
|
||||
azure-devops-node-api: ^14.0.0
|
||||
better-sqlite3: ^11.0.0
|
||||
dockerode: ^4.0.0
|
||||
example-app: "link:../app"
|
||||
@@ -31575,6 +31573,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-md4@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "js-md4@npm:0.3.2"
|
||||
checksum: aa7b7cbd738b105f86fc0fa50ce2a7a6b6b329ff3f713d1bd38b12c2a72929bab5a16e658a03830faa8d656356aa89b4f77f1fbf05ffa8d095bda6d831441c2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "js-tokens@npm:4.0.0"
|
||||
@@ -35167,11 +35172,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"node-mocks-http@npm:^1.0.0":
|
||||
version: 1.16.0
|
||||
resolution: "node-mocks-http@npm:1.16.0"
|
||||
version: 1.16.1
|
||||
resolution: "node-mocks-http@npm:1.16.1"
|
||||
dependencies:
|
||||
"@types/express": ^4.17.21
|
||||
"@types/node": "*"
|
||||
accepts: ^1.3.7
|
||||
content-disposition: ^0.5.3
|
||||
depd: ^1.1.0
|
||||
@@ -35182,7 +35185,15 @@ __metadata:
|
||||
parseurl: ^1.3.3
|
||||
range-parser: ^1.2.0
|
||||
type-is: ^1.6.18
|
||||
checksum: 21ccf1ecaaa6ee0f7c061a7063f59d59c9793a485a907c09c83ff73b12f205ef87537022a4ba8ad937d07454ee0450290093d4a66a4df21e7e8b3696d23e1a7d
|
||||
peerDependencies:
|
||||
"@types/express": ^4.17.21 || ^5.0.0
|
||||
"@types/node": "*"
|
||||
peerDependenciesMeta:
|
||||
"@types/express":
|
||||
optional: true
|
||||
"@types/node":
|
||||
optional: true
|
||||
checksum: 198030725eac236062eb3547a7d5cec2d6f2d44bf05efab0ea621e90f671d6966dca2873faaea9bcbb5dde92a222aff0610a01d453c2f76b0bc5dbdf4bd8057a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -38048,7 +38059,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:6.13.0, qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.9.1, qs@npm:^6.9.4":
|
||||
"qs@npm:6.13.0, qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.9.4":
|
||||
version: 6.13.0
|
||||
resolution: "qs@npm:6.13.0"
|
||||
dependencies:
|
||||
@@ -43100,14 +43111,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typed-rest-client@npm:^1.8.4":
|
||||
version: 1.8.4
|
||||
resolution: "typed-rest-client@npm:1.8.4"
|
||||
"typed-rest-client@npm:^2.0.1":
|
||||
version: 2.0.2
|
||||
resolution: "typed-rest-client@npm:2.0.2"
|
||||
dependencies:
|
||||
qs: ^6.9.1
|
||||
des.js: ^1.1.0
|
||||
js-md4: ^0.3.2
|
||||
qs: ^6.10.3
|
||||
tunnel: 0.0.6
|
||||
underscore: ^1.12.1
|
||||
checksum: 238e2139724310fed39756ae734fbc811e803abaa7598720ad6cc5ab7f160d415d8107551422b186f6b2e08c5761c0e7b66ca47d60b2ccd952bb5e20d3b92f3c
|
||||
checksum: f1d9a0ffd5b7266f01df9ea4eb08b8da04793cf6902d82b3c6a46d1328cdefe80bb0cfc8f96afa645077c234ec218d8b16f642da35c1e2f1adf3d61a5904d8d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user