Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
diff --git a/rules/no-undeclared-imports.js b/rules/no-undeclared-imports.js
index 17331d2fced77d13e9eb76631210e26524c83f26..5cf8935c11b07d38e2f548a9307a6ef8a2e69913 100644
--- a/rules/no-undeclared-imports.js
+++ b/rules/no-undeclared-imports.js
@@ -22,6 +22,20 @@ const visitImports = require('../lib/visitImports');
const minimatch = require('minimatch');
const { execFileSync } = require('child_process');

+/**
+ * Runs the Yarn CLI with the provided arguments in a cross-platform way.
+ *
+ * @param {string[]} args
+ * @param {import('child_process').ExecFileSyncOptions} options
+ */
+function runYarn(args, options) {
+ if (process.platform === 'win32') {
+ execFileSync('cmd', ['/c', 'yarn', ...args], options);
+ return;
+ }
+ execFileSync('yarn', args, options);
+}
+
const depFields = /** @type {const} */ ({
dep: 'dependencies',
dev: 'devDependencies',
@@ -186,7 +200,7 @@ function addMissingImports(toAdd, packages, localPkg) {
// The security implication of this is a bit interesting, as crafted add-import
// directives could be used to install malicious packages. However, the same is true
// for adding malicious packages to package.json, so there's no significant difference.
- execFileSync('yarn', ['add', ...(flag ? [flag] : []), ...namesWithQuery], {
+ runYarn(['add', ...(flag ? [flag] : []), ...namesWithQuery], {
cwd: localPkg.dir,
stdio: 'inherit',
});
@@ -211,7 +225,7 @@ function removeInlineImports(toInline, localPkg) {
}
}
if (toRemove.size > 0) {
- execFileSync('yarn', ['remove', ...toRemove], {
+ runYarn(['remove', ...toRemove], {
cwd: localPkg.dir,
stdio: 'inherit',
});
@@ -265,7 +279,7 @@ function addForwardedInlineImports(toInline, localPkg) {
const namesWithQuery = [...byName.entries()].map(
([name, query]) => `${name}@${query}`,
);
- execFileSync('yarn', ['add', ...(flag ? [flag] : []), ...namesWithQuery], {
+ runYarn(['add', ...(flag ? [flag] : []), ...namesWithQuery], {
cwd: localPkg.dir,
stdio: 'inherit',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: '3.8'

services:
sonarqube:
image: sonarqube:lts
ports:
- '9000:9000'
environment:
- SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
ulimits:
nofile:
soft: 65536
hard: 65536

volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
},
"resolutions": {
"@types/react": "^18",
"@types/react-dom": "^18"
"@types/react-dom": "^18",
"@backstage/eslint-plugin@npm:^0.1.12": "patch:@backstage/eslint-plugin@npm%3A0.1.12#./.yarn/patches/@backstage-eslint-plugin-npm-0.1.12-dce2230ed2.patch"
},
"prettier": "@backstage/cli/config/prettier",
"lint-staged": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ This module provides [Backstage](https://backstage.io/) template [actions](https

The following actions are currently supported in this module:

- Create a SonarQube [project](https://docs.sonarqube.org/latest/user-guide/project-page/)
- `sonarqube:project:create` – Creates projects using shared SonarQube credentials that live in Backstage configuration (**recommended**)
- `sonarqube:create-project` – Legacy action that allows passing SonarQube credentials through template input (**deprecated**, kept for backward compatibility)

## Prerequisites

Expand Down Expand Up @@ -36,24 +37,58 @@ backend.start();

## Configuration

Add the SonarQube actions to your templates, see the [examples](./examples/templates) directory of this repository for complete usage examples
Configure the SonarQube integration once in your Backstage backend:

```yaml title="app-config.yaml"
sonarqube:
baseUrl: https://sonarqube.company.com
token:
$env: SONARQUBE_TOKEN # or any other secret source
```

Then add the module to your backend and reference the recommended action in your templates:

```yaml
action: sonarqube:create-project
id: 'create-sonar-project'
name: 'Create SonarQube Project'
action: sonarqube:project:create
id: create-sonar-project
name: Create SonarQube Project
input:
baseUrl: 'https://sonarqube.com'
token: '4518a13e-093f-4b66-afac-46a1aece3149'
name: 'My SonarQube Project'
key: 'my-sonarqube-project'
branch: 'main'
visibility: 'public'
projectKey: my-sonarqube-project
projectName: My SonarQube Project
organization: demo-org
visibility: private
```

## Usage
See the [examples](./examples/templates) directory for complete usage examples.

If you still rely on the legacy `sonarqube:create-project` action you can continue to pass the credentials directly inside the template input (see below), but please plan to migrate to the new action soon.

## Actions

### Action: sonarqube:project:create (recommended)

This action uses the shared SonarQube configuration, so no credentials need to be passed through the template.

#### Input

| Parameter Name | Type | Required | Description | Example |
| -------------- | :----: | :------: | ------------------------------------------------------------------------------------- | ---------- |
| projectKey | string | Yes | Unique SonarQube project key | my-service |
| projectName | string | Yes | Display name for the project | My Service |
| organization | string | No | SonarQube organization key (only needed on SonarQube Cloud) | demo-org |
| visibility | string | No | Whether the project is `public` or `private`. Defaults to the SonarQube server value. | private |

### Action: sonarqube:create-project
#### Output

| Name | Type | Description |
| ---------- | :----: | -------------------------------------------- |
| projectUrl | string | SonarQube project URL created by this action |

### Action: sonarqube:create-project (legacy / deprecated)

> **Warning**
>
> This action is kept for backwards compatibility. Please migrate to `sonarqube:project:create`, which avoids handling credentials in templates.

#### Input

Expand All @@ -68,13 +103,61 @@ input:
| username | string | No | SonarQube username | |
| password | string | No | SonarQube password | |

> **Warning**
>
> Either the `token` or `username` and `password` input combination are required.
> If the three of them are provided, the `token` will take precedence
> If the three of them are provided, the `token` will take precedence.

#### Output

| Name | Type | Description |
| ---------- | :----: | -------------------------------------------- |
| projectUrl | string | SonarQube project URL created by this action |

## Programmatic usage

The package also exports a light-weight `SonarQubeClient` that can be used in custom code or other actions:

```ts
import { createSonarQubeClient } from '@backstage-community/plugin-scaffolder-backend-module-sonarqube';

const sonarQube = createSonarQubeClient({
baseUrl: 'https://sonarqube.company.com',
token: process.env.SONARQUBE_TOKEN!,
});

await sonarQube.createProject({
project: 'my-service',
name: 'My Service',
});
```

## Local testing

You can validate the new config-driven action against a local SonarQube instance.

1. Start SonarQube using the provided compose file:

```bash
cd workspaces/scaffolder-backend-module-sonarqube
docker compose -f docker-compose.test.yml up -d
```

SonarQube will be available at `http://localhost:9000`.

2. Create an admin token in SonarQube (or reuse an existing one) and reference it from `plugins/scaffolder-backend-module-sonarqube/app-config.local.yaml`:

```yaml
sonarqube:
baseUrl: http://localhost:9000
token: ${SONARQUBE_TOKEN}
```

Export `SONARQUBE_TOKEN` in your shell so the config can resolve it.

3. Run the action tests from the plugin directory:

```bash
cd plugins/scaffolder-backend-module-sonarqube
yarn test src/actions/createConfiguredSonarQubeProject.test.ts --runTestsByPath
```

When the base URL and token are available (either via env vars or `app-config.local.yaml`), the test suite automatically enables the “live” scenario and exercises the action against your running SonarQube container in addition to the mocked unit tests.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
},
"dependencies": {
"@backstage/backend-plugin-api": "^1.4.4",
"@backstage/config": "^1.1.1",
"@backstage/plugin-scaffolder-node": "^0.12.0",
"cross-fetch": "^4.1.0",
"yaml": "^2.3.3"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,53 @@

```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';

// Warning: (ae-missing-release-tag) "CREATE_CONFIGURED_SONARQUBE_PROJECT_ID" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const CREATE_CONFIGURED_SONARQUBE_PROJECT_ID =
'sonarqube:project:create';

// @public
export const createConfiguredSonarQubeProjectAction: (
config: Config,
) => TemplateAction<
{
projectKey: string;
projectName: string;
organization?: string | undefined;
visibility?: 'public' | 'private' | undefined;
},
{
projectUrl: string;
},
'v2'
>;

// Warning: (ae-missing-release-tag) "CreateProjectParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface CreateProjectParams {
// (undocumented)
name: string;
// (undocumented)
organization?: string;
// (undocumented)
project: string;
// (undocumented)
visibility?: 'public' | 'private';
}

// Warning: (ae-missing-release-tag) "createSonarQubeClient" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createSonarQubeClient(
options: SonarQubeClientOptions,
): SonarQubeClient;

// @public @deprecated (undocumented)
export const createSonarQubeProjectAction: () => TemplateAction<
{
baseUrl: string;
Expand All @@ -24,7 +68,46 @@ export const createSonarQubeProjectAction: () => TemplateAction<
'v2'
>;

// Warning: (ae-missing-release-tag) "GenerateTokenParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface GenerateTokenParams {
// (undocumented)
name: string;
// (undocumented)
organization?: string;
// (undocumented)
projectKey: string;
// (undocumented)
type: 'PROJECT_ANALYSIS_TOKEN';
}

// @public (undocumented)
const scaffolderModuleSonarqubeActions: BackendFeature;
export default scaffolderModuleSonarqubeActions;

// Warning: (ae-missing-release-tag) "SonarQubeClient" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class SonarQubeClient {
constructor(options: SonarQubeClientOptions);
// (undocumented)
createProject(params: CreateProjectParams): Promise<{
key: string;
}>;
// (undocumented)
generateToken(params: GenerateTokenParams): Promise<{
token: string;
}>;
}

// Warning: (ae-missing-release-tag) "SonarQubeClientOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface SonarQubeClientOptions {
// (undocumented)
baseUrl: string;
// (undocumented)
token: string;
}
```
Loading
Loading