diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..bf30a23 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +/.github/ @Mararok \ No newline at end of file diff --git a/.github/actions/publish/action.yaml b/.github/actions/publish/action.yaml deleted file mode 100644 index cc66ca0..0000000 --- a/.github/actions/publish/action.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Test -description: 'execute tests' -runs: - using: 'composite' - steps: - # Bump version - - name: Bump package version - shell: bash - run: | - yarn version $RELEASE_TYPE - echo "RELEASE_TAG=latest" >> $GITHUB_ENV - echo "NEW_VERSION=$(jq -r '.version' < package.json)" >> $GITHUB_ENV - env: - RELEASE_TYPE: ${{ inputs.releaseType }} - - - name: Update CHANGELOG.md - uses: 'zen8sol/update-changelog-action@0.1.5' - with: - newVersion: '${{ env.NEW_VERSION }}' - - # Publish package - - name: Publish - shell: bash - run: | - echo -e "\nnpmAuthToken: '${{ env.NODE_AUTH_TOKEN }}'" >> ./.yarnrc.yml - yarn npm publish --access public --tag ${{ env.RELEASE_TAG }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - # Commit - - name: Commit CHANGELOG.md and package.json changes and create tag - shell: bash - run: | - git config --global user.email "153127894+hexancore-bot@users.noreply.github.com" - git config --global user.name "Hexancore Bot" - git add "package.json" - git add "CHANGELOG.md" - git commit -m "chore: release ${{ env.NEW_VERSION }}" - git tag -m 'new version' ${{ env.NEW_VERSION }} - git push --follow-tags diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 13c2a74..fd65974 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -1,7 +1,7 @@ name: Pull Request on: pull_request: - branches: [ master, main ] + branches: [main] jobs: check: @@ -10,10 +10,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Yarn install - uses: hexancore/yarn-ci-install@0.1.0 + uses: hexancore/yarn-ci-install@ba9baf131eba84b6c86efb46375a530a3098bb04 - name: Lint run: yarn lint - name: Test uses: ./.github/actions/test - name: Build - run: yarn build \ No newline at end of file + run: yarn build diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml deleted file mode 100644 index 6b9e7d6..0000000 --- a/.github/workflows/prepare-release.yaml +++ /dev/null @@ -1,70 +0,0 @@ -name: Prepare Release - -on: - workflow_dispatch: - inputs: - releaseType: - description: 'Release type (one of): patch, minor, major' - required: true - type: choice - options: - - patch - - minor - - major - -jobs: - prepare-release: - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ env.HEXANCORE_BOT_TOKEN }} - env: - HEXANCORE_BOT_TOKEN: ${{ secrets.HEXANCORE_BOT_TOKEN }} - - - name: Configure Git user - run: | - git config --global user.email "153127894+hexancore-bot@users.noreply.github.com" - git config --global user.name "Hexancore Bot" - - - name: Configure GPG - run: | - echo "${{ secrets.HEXANCORE_BOT_GPG_PRIVATE }}" | base64 -d | gpg --import - echo "${{ secrets.HEXANCORE_BOT_GPG_PASS }}" | gpg --passphrase-fd 0 --batch --yes --pinentry-mode loopback --command-fd 0 - git config --global commit.gpgsign true - git config --global user.signingkey ${{ secrets.HEXANCORE_BOT_GPG_KEY_ID }} - - # Bump version - - name: Bump package version - shell: bash - run: | - yarn version $RELEASE_TYPE - echo "RELEASE_TAG=latest" >> $GITHUB_ENV - echo "NEW_VERSION=$(jq -r '.version' < package.json)" >> $GITHUB_ENV - env: - RELEASE_TYPE: ${{ inputs.releaseType }} - - - name: Update CHANGELOG.md - uses: 'zen8sol/update-changelog-action@0.1.5' - with: - newVersion: '${{ env.NEW_VERSION }}' - - # Commit - - name: Commit CHANGELOG.md and package.json changes - shell: bash - run: | - git add "package.json" - git add "CHANGELOG.md" - git commit -m "chore: release ${{ env.NEW_VERSION }}" - git push git push --set-upstream origin chore-prepare-release - - name: Create Pull Request - run: | - curl \ - -X POST \ - -H "Authorization: token ${{ secrets.HEXANCORE_BOT_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/${{ github.repository }}/pulls \ - -d '{"title":"Prepare new version release: ${{ env.NEW_VERSION }}","head":"chore-prepare-release","base":"main","body":"Prepare new version release: ${{ env.NEW_VERSION }}"}' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8e47c72..8d21250 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,13 +12,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - token: ${{ env.HEXANCORE_BOT_TOKEN }} - env: - HEXANCORE_BOT_TOKEN: ${{ secrets.HEXANCORE_BOT_TOKEN }} - name: Yarn install - uses: hexancore/yarn-ci-install@0.1.0 + uses: hexancore/yarn-ci-install@ba9baf131eba84b6c86efb46375a530a3098bb04 # Publish package - name: Publish diff --git a/CHANGELOG.md b/CHANGELOG.md index 8649b3a..1fbec34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## 0.1.2 + ### Changed - Release with gpg sign + generate provenance statements diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f36c005 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2023 Andrzej Wasiak + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/package.json b/package.json index d447b0f..d6233d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hexancore/cli", - "version": "0.1.0", + "version": "0.1.2", "engines": { "node": ">=20" }, @@ -8,39 +8,18 @@ "description": "CLI of Hexancore framework", "author": { "name": "Andrzej Wasiak", - "email": "contact@andrzejwasiak.pl", "url": "https://andrzejwasiak.pl" }, "license": "MIT", "main": "./lib/index.js", - "types": "./lib/index.d.ts", - "typesVersions": { - "*": { - ".": [ - "./lib/index.d.ts" - ] - } - }, - "exports": { - ".": { - "import": { - "types": "./lib/index.d.ts", - "default": "./lib/index.js" - }, - "default": { - "types": "./lib/index.d.ts", - "default": "./lib/index.js" - } - } - }, "repository": { "type": "git", - "url": "git+https://github.com/hexancore/cli.git" + "url": "https://github.com/hexancore/cli.git" }, "publishConfig": { "access": "public" }, - "homepage": "https://github.com/hexancore/cli.git", + "homepage": "https://github.com/hexancore/cli", "packageManager": "yarn@4.1.0", "scripts": { "hcli": "node --optimize_for_size --max_old_space_size=460 --gc_interval=100 ./lib/index.js", @@ -96,5 +75,10 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "5.3.3" - } + }, + "files": [ + "bin", + "lib", + "templates" + ] } diff --git a/src/Command/Package/PackageBumpVersionCommandHandler.ts b/src/Command/Package/PackageBumpVersionCommandHandler.ts new file mode 100644 index 0000000..4795081 --- /dev/null +++ b/src/Command/Package/PackageBumpVersionCommandHandler.ts @@ -0,0 +1,70 @@ +import { ERR, OK, OKA, type AR, type R } from '@hexancore/common'; +import { LocalDate, ZoneOffset } from '@js-joda/core'; +import { inject, injectable } from 'inversify'; +import { FilesystemHelper, type FileItem } from '../../Util'; +import { Changelog } from '../../Util/Changelog/Changelog'; + +interface PackageBumpVersionCommandOptions { + dryRun?: true; + newVersion: string; + releaseDate?: string +} + +@injectable() +export class PackageBumpVersionCommandHandler { + public constructor( + @inject(FilesystemHelper) private fs: FilesystemHelper + ) { + } + + public execute(options: PackageBumpVersionCommandOptions): AR { + return this.fs.readJson('package.json').onOk(packageJson => { + packageJson.version = options.newVersion; + const repositoryUrl = this.extractReposiotryUrlFromPackageJson(packageJson); + if (repositoryUrl.isError()) { + return ERR(repositoryUrl.e); + } + const releaseDate: string = options.releaseDate ?? LocalDate.now(ZoneOffset.UTC).toString(); + + const updatedChangelog = this.updateChangelog(repositoryUrl.v, options.newVersion, releaseDate); + return this.save([ + { path: 'package.json', content: JSON.stringify(packageJson, null, 2) }, + { path: 'CHANGELOG.md', content: () => updatedChangelog }, + ], options.dryRun); + }); + } + + private extractReposiotryUrlFromPackageJson(packageJson: Record): R { + const repositoryUrl = packageJson.repository?.url; + if (typeof repositoryUrl !== 'string') { + return ERR('core.cli.package.empty_repository_url'); + } + return OK(repositoryUrl.replace('.git', '')); + } + + private updateChangelog(repositoryUrl: string, newVersion: string, releaseDate): AR { + return this.fs.getFileLines('CHANGELOG.md').onOk((lines) => { + const changelog = new Changelog(repositoryUrl, lines); + changelog.updateWithNewVersion(newVersion, releaseDate); + return changelog.toString(); + }); + } + + private save(files: FileItem[], dryRun: boolean): AR { + if (dryRun) { + return OKA(files).onEachAsArray((item) => { + if (typeof item.content === 'function') { + return item.content().onOk((content) => { + console.log('Files:\n' + `### ${item.path} ###\n${content}\n### END ###\n\n`); + }); + } else { + console.log('Files:\n' + `### ${item.path} ###\n${item.content}\n### END ###\n\n`); + return OK(undefined); + } + + }).onOk(() => { }); + } + + return this.fs.outputFiles(files); + } +} \ No newline at end of file diff --git a/src/Command/registerCommands.ts b/src/Command/registerCommands.ts index 9d4413c..ceeac5f 100644 --- a/src/Command/registerCommands.ts +++ b/src/Command/registerCommands.ts @@ -1,10 +1,11 @@ import type { Command, CommandUnknownOpts } from '@commander-js/extra-typings'; import { StdErrors, type AR } from '@hexancore/common'; import type { Container } from 'inversify'; -import { HcModuleHelper, ProjectHelper, PromptHelper, printError, styles } from '../Util'; +import { FilesystemHelper, HcModuleHelper, ProjectHelper, PromptHelper, printError, styles } from '../Util'; import { MakeModuleCommandHandler } from './Make/MakeModuleCommandHandler'; import { MakeModuleMessageCommandHandler } from './Make/MakeModuleMessageCommandHandler'; import { PulumiMakeProjectCommandHandler } from './Pulumi/MakeProject/PulumiMakeProjectCommandHandler'; +import { PackageBumpVersionCommandHandler } from './Package/PackageBumpVersionCommandHandler'; async function actionWrapper(action: () => AR): Promise { (await action()).onErr((e) => { @@ -34,13 +35,13 @@ function makeModuleMessage(cli: Command, c: Container): void { .action((options) => actionWrapper(() => command.execute(options))); } -function make(cli: Command, c: Container): void { - const make = cli.command('make').description('display subcommand list'); +function makeCommands(cli: Command, c: Container): void { + const make = cli.command('make').description('display subcommand list'); makeModule(make, c); makeModuleMessage(make, c); } -function pulumi(cli: Command, c: Container): void { +function pulumiCommands(cli: Command, c: Container): void { const pulumi = cli.command('pulumi').description('display subcommand list'); const make = pulumi.command('make').description('[project]'); @@ -52,12 +53,25 @@ function pulumi(cli: Command, c: Container): void { .action((options) => actionWrapper(() => makeProjectCommand.execute(options))); } +function packageCommands(cli: Command, c: Container): void { + const commandGroup = cli.command('package').description('display subcommand list'); + commandGroup.command('bump-version') + .description('Bumps package version') + .option('--dryRun', 'Prints updated files', false) + .argument('newVersion', 'new version of package') + .action((newVersion, options) => actionWrapper(() => { + const handler = new PackageBumpVersionCommandHandler(c.get(FilesystemHelper)); + return handler.execute({ newVersion, ...options }); + })); +} + export default function (cli: Command, c: Container): void { cli.configureHelp({ subcommandTerm: (cmd: CommandUnknownOpts) => { return styles.primary(cmd.name()); } }); - make(cli, c); - pulumi(cli, c); + makeCommands(cli, c); + pulumiCommands(cli, c); + packageCommands(cli, c); } diff --git a/src/Util/Changelog/Changelog.ts b/src/Util/Changelog/Changelog.ts new file mode 100644 index 0000000..dec95e3 --- /dev/null +++ b/src/Util/Changelog/Changelog.ts @@ -0,0 +1,76 @@ +import fs from 'fs'; + +export class Changelog { + + public constructor( + public readonly repositoryUrl: string, + public lines: string[] + ) { + + } + + protected genCompareRevLink(oldRev: string | null, newRev: string | 'HEAD'): string { + const label = newRev === 'HEAD' ? '[unreleased]' : `[${newRev}]`; + const linkType = oldRev === null ? `/releases/tag/${newRev}` : `compare/${oldRev}...${newRev}`; + return `${label} ${this.repositoryUrl}/${linkType} `; + } + + public updateWithNewVersion(newVersion: string, releaseDate: string): void { + const newVersionChangesReplaces = [ + '## [Unreleased]\n', + `## [${newVersion}] - ${releaseDate}`, + ].join('\n'); + + for (let i = 0; i < this.lines.length; i++) { + if (this.lines[i] === '## [Unreleased]') { + this.lines[i] = newVersionChangesReplaces; + break; + } + } + + this.updateFooter(newVersion); + } + + private updateFooter(newVersion: string): void { + let foundOldVersion = false; + for (let i = this.lines.length - 1; i >= 0; i--) { + if (this.lines[i].startsWith('[unreleased]')) { + if (this.lines.length < i + 1) { + throw new Error('`[unreleased]` line exists, but previous version line not' + ); + } + + const extractOldVersionRegex = /\[([0-9.]+)\]/; + // [0.1.0] ... + const oldVersionTagString = this.lines[i + 1].split(' ', 1)[0]; + const oldVersionMatch = oldVersionTagString.match(extractOldVersionRegex); + + if (!oldVersionMatch) { + throw new Error('Invalid old version format: ' + oldVersionTagString); + } + + const oldVersion = oldVersionMatch[1]; + + this.lines[i] = [ + this.genCompareRevLink(newVersion, 'HEAD'), + this.genCompareRevLink(oldVersion, newVersion), + ].join('\n'); + foundOldVersion = true; + break; + } + } + + if (!foundOldVersion) { + this.lines.push( + [ + this.genCompareRevLink(newVersion, 'HEAD'), + this.genCompareRevLink(null, newVersion), + ].join('\n') + ); + } + } + + public toString(): string { + return this.lines.join('\n'); + } +} \ No newline at end of file diff --git a/src/Util/Code/Code.ts b/src/Util/Code/Code.ts index 9665292..bf51111 100644 --- a/src/Util/Code/Code.ts +++ b/src/Util/Code/Code.ts @@ -120,10 +120,7 @@ export class Code { } return this.render().onOk((output) => { - return ARW(this.fs.mkdirs(output.dirs)) - .onOk(() => { - this.fs.outputFiles(output.files); - }); + return this.fs.mkdirs(output.dirs).onOk(() => this.fs.outputFiles(output.files)); }); } diff --git a/src/Util/Filesystem/FilesystemHelper.ts b/src/Util/Filesystem/FilesystemHelper.ts index 552cd2e..b6cbc1a 100644 --- a/src/Util/Filesystem/FilesystemHelper.ts +++ b/src/Util/Filesystem/FilesystemHelper.ts @@ -2,39 +2,55 @@ import * as fs from 'fs-extra'; import { injectable } from 'inversify'; import type { FileItem } from './FileItem'; import { glob, type Options } from 'fast-glob'; -import { ARW, type AR } from '@hexancore/common'; +import { ARW, type AR, ERR } from '@hexancore/common'; @injectable() export class FilesystemHelper { - + public pathExists(path: string): boolean { return fs.pathExistsSync(path); } - public async mkdirs(dirs: string[]): Promise { - await Promise.all(dirs.map((file: string) => this.mkdir(file))); + public mkdirs(dirs: string[]): AR { + return ARW(Promise.all(dirs.map((file: string) => this.mkdir(file).p))).onOk((results) => { + const errors = results.filter((r) => r.isError()); + if (errors.length > 0) { + return ERR('core.cli.fs.mkdirs', 500, errors); + } + }); } - public async mkdir(dir: string): Promise { + public mkdir(dir: string): AR { const mode = 0o2775; - await fs.ensureDir(dir, mode); + return ARW(fs.ensureDir(dir, mode)); } - public async outputFiles(files: FileItem[]): Promise { - await Promise.all(files.map((file: FileItem) => this.outputFile(file))); + public outputFiles(files: FileItem[]): AR { + return ARW(Promise.all(files.map((file: FileItem) => this.outputFile(file).p))).onOk((results) => { + const errors = results.filter((r) => r.isError()); + if (errors.length > 0) { + return ERR('core.cli.fs.output_files', 500, errors); + } + }); } - public async outputFile(file: FileItem): Promise { - return fs.outputFile(file.path, file.content); + public outputFile(path: FileItem | string, content?: string | (() => AR)): AR { + const file = typeof path === 'object' ? path : { path, content }; + return ARW(fs.outputFile(file.path, file.content)); } - public async readFile(path: string): Promise { - const buffer = await fs.readFile(path); - return buffer.toString('utf8'); + public readTextFile(path: string, encoding: BufferEncoding = 'utf8'): AR { + return ARW(fs.readFile(path)).onOk((buffer) => { + return buffer.toString(encoding); + }); } public readJson(path: string): AR { - return ARW(this.readFile(path)).onOk(content => JSON.parse(content)); + return this.readTextFile(path).onOk(content => JSON.parse(content)); + } + + public getFileLines(path: string): AR { + return this.readTextFile(path).onOk((text) => text.split('\n')); } public fastGlob(patterns: string | string[], options: Options): AR { diff --git a/src/index.ts b/src/index.ts index 6b7f238..929ad73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ import 'reflect-metadata'; import registerCommands from './Command/registerCommands'; import { CLI_VERSION, DI } from './Constants'; import { HcCliContainerBuilder } from './DI'; -import { styles, type IMonorepoHelper } from './Util'; function main() { const cli = new Command(); @@ -13,11 +12,6 @@ function main() { const cwd = process.cwd().replaceAll('\\', '/'); const c = HcCliContainerBuilder.create(cwd).build(); - - if (!c.get(DI.monorepoHelper).isMonorepoRootDir(cwd)) { - console.log(styles.danger('Current dir is not monorepo root dir!')); - return; - } registerCommands(cli, c); return cli.parseAsync(process.argv); } diff --git a/tsconfig.build.json b/tsconfig.build.json index 200d6d4..52afdd8 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,11 @@ { "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "incremental": false, + }, "files": null, "include": ["src/**/*"] } diff --git a/tsconfig.json b/tsconfig.json index 371a4d3..2311b75 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "importHelpers": true, "declaration": true, "declarationMap": true, + "sourceMap": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true,