POC - JS-601 - Publish RSPEC as an npm package

This commit is contained in:
Eric Morand 2025-03-06 17:03:57 +01:00
parent 8586551b59
commit 77f34c3a7b
3 changed files with 235 additions and 0 deletions

60
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,60 @@
on:
release:
types:
- published
jobs:
publish:
permissions:
contents: read
id-token: write # required for SonarSource/vault-action-wrapper
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_NAME: ${{ github.event.release.name }}
BUILD_NAME: 'rspec'
ARTIFACTORY_REPOSITORY_NAME: 'sonarsource-npm-public-releases'
steps:
- name: Fetch the secrets
id: secrets
uses: SonarSource/vault-action-wrapper@v3
with:
secrets:
development/artifactory/token/SonarSource-rspec-promoter access_token | promoter_access_token;
development/kv/data/npmjs sonartech_npm_token | npm_token;
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Setup the registries
run: |
npm config set //registry.npmjs.org/:_authToken=${{ fromJSON(steps.secrets.outputs.vault).npm_token }}
npm config set //repox.jfrog.io/artifactory/api/npm/:_authToken=${{ fromJSON(steps.secrets.outputs.vault).promoter_access_token }}
- name: Build the package
run: |
(cd rspec-tools && npm i)
VERSION=${RELEASE_TAG} node rspec-tools/build-package.mjs
cp LICENSE build
- name: Install JFrog CLI
uses: SonarSource/jfrog-setup-wrapper@v3
- name: Publish the package to Artifactory
run: |
jfrog config add repox --url https://repox.jfrog.io --access-token ${{ fromJSON(steps.secrets.outputs.vault).promoter_access_token }}
jfrog config use repox
jfrog npm-config --repo-resolve npm --repo-deploy $ARTIFACTORY_REPOSITORY_NAME
cd build
jfrog npm publish --build-name $BUILD_NAME --build-number $RELEASE_TAG --dry-run
jfrog rt build-add-git $BUILD_NAME $RELEASE_TAG
jfrog rt build-publish --dry-run $BUILD_NAME $RELEASE_TAG
jfrog rt build-promote --dry-run --status released $BUILD_NAME $RELEASE_TAG $ARTIFACTORY_REPOSITORY_NAME
- name: Publish the package to npm
run: |
cd build
[ ${{ github.event.release.prerelease }} == true ] && TAG="next" || TAG="latest"
npm publish --tag=$TAG --access=public --dry-run

View File

@ -0,0 +1,170 @@
import {readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, rmSync} from "node:fs";
import {join} from "node:path";
import createASCIIDoctor from "asciidoctor";
const asciiDoctor = createASCIIDoctor();
/**
* @typedef BaseRuleRepresentation
*/
/**
* @typedef RuleRepresentation
* @property {string} title
*/
/**
* @typedef Metadata
* @property {string} documentation
*/
/**
* @typedef Rule
* @property {string} name
* @property {string} language
* @property {string} title
* @property {string} type
* @property {Metadata} metadata
*/
/**
* @param {string} directoryPath
* @return {Array<{
* rule: Rule;
* metadata: Metadata;
* }>}
*/
const readDirectory = (directoryPath) => {
const entries = readdirSync(directoryPath);
/**
* @param {string} rulePath
* @param {string} name
* @return {Array<{
* rule: Rule;
* metadata: Metadata;
* }>}
*/
const processRulePath = (rulePath, name) => {
/**
* @type {BaseRuleRepresentation}
*/
const baseRuleRepresentation = JSON.parse(readFileSync(join(rulePath, 'metadata.json'), "utf-8"));
const languageEntries = readdirSync(join(rulePath), {
withFileTypes: true
});
/**
* @type {Array<Rule>}
*/
const results = [];
languageEntries.forEach((languageEntry) => {
if (languageEntry.isDirectory() && languageEntry.name === "javascript") {
const languagePath = join(rulePath, languageEntry.name);
const metadataPath = join(languagePath, 'metadata.json');
if (existsSync(metadataPath)) {
/**
* @type {RuleRepresentation}
*/
const ruleRepresentation = {
...baseRuleRepresentation,
...JSON.parse(readFileSync(metadataPath, "utf-8"))
};
const documentationPath = join(languagePath, 'rule.adoc');
/**
* @type {string}
*/
let documentation;
if (existsSync(documentationPath)) {
documentation = readFileSync(documentationPath, "utf-8");
documentation = asciiDoctor.convert(documentation, {
base_dir: languagePath,
safe: 0
});
}
results.push({
rule: {
name,
language: languageEntry.name,
...ruleRepresentation
},
metadata: {
documentation
}
});
}
}
});
return results;
};
const results = [];
entries.forEach((entry) => {
const rulePath = join(directoryPath, entry);
results.push(...processRulePath(rulePath, entry));
});
return results;
};
const rules = readDirectory('rules');
const moduleContent = `const registry = new Map([
${rules.map((rule) => {
return `['${rule.rule.language}/${rule.rule.name}', ${JSON.stringify(rule)}]`
})}
]);
export const getRules = () => {
return [...registry.values()].map((entry) => entry.rule);
};
export const getMetadata = (rule) => {
const entry = registry.get(rule.language + '/' + rule.name);
return entry.metadata;
};
`;
rmSync('build', {
force: true,
recursive: true
});
mkdirSync('build', {});
writeFileSync('build/index.mjs', moduleContent, {});
writeFileSync('build/index.d.ts', `export type Metadata = {
readonly documentation: string;
};
export type Rule = {
readonly language: string;
readonly name: string;
readonly title: string;
readonly type: string;
readonly defaultQualityProfiles: Array<string>;
readonly status: "ready" | "deprecated" | "closed";
};
export declare const getRules: () => Array<Rule>;
export declare const getMetadata: (rule: Rule) => Metadata;
`, {});
writeFileSync('build/package.json', JSON.stringify({
main: "index.mjs",
types: "index.d.ts",
name: "@sonar/rspec",
version: process.env.VERSION || "SNAPSHOT"
}));

5
rspec-tools/package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"asciidoctor": "^3.0.4"
}
}