diff --git a/src/publish-beta/command.ts b/src/publish-beta/command.ts deleted file mode 100644 index 591b940..0000000 --- a/src/publish-beta/command.ts +++ /dev/null @@ -1,28 +0,0 @@ -// publish-beta/command.ts - -import process from 'node:process'; -import { debug } from 'debug'; -import { getInput } from '@actions/core'; - -import { publishComment } from './github'; -import { isPackageAnAPI, packageJSONUpdate } from './package'; - -const log = debug('action:command'); -export default async function (): Promise { - const command = getInput('command').toLowerCase(); - log('Action starts:', command); - - if (command === 'publish-comment' && process.env['NEW_PACKAGE_VERSION']) { - return publishComment(process.env['NEW_PACKAGE_VERSION']); - } - - if (command === 'is-api') { - return isPackageAnAPI(process.cwd()); - } - - if (command === 'generate-beta-version') { - return packageJSONUpdate(process.cwd()); - } - log('No valid command received: action-publish-beta [is-api | generate-beta-version | publish-comment]'); - throw new Error('no valid command'); -} diff --git a/src/publish-beta/compile.ts b/src/publish-beta/compile.ts new file mode 100644 index 0000000..ee477eb --- /dev/null +++ b/src/publish-beta/compile.ts @@ -0,0 +1,14 @@ +// publish-beta/compile.ts + +import childProcess from 'node:child_process'; +import util from 'node:util'; +import debug from 'debug'; + +const log = debug('publish-beta:compile'); + +const exec = util.promisify(childProcess.exec); +export default async function (directory: string): Promise { + log('compile starting'); + await exec('tsc --outDir dist --sourceMap true --declarationMap true', { cwd: directory }); + log('compile completed'); +} diff --git a/src/publish-beta/files.spec.ts b/src/publish-beta/files.spec.ts new file mode 100644 index 0000000..c3adf12 --- /dev/null +++ b/src/publish-beta/files.spec.ts @@ -0,0 +1,68 @@ +// publish-beta/files.spec.ts + +import { strict as assert } from 'node:assert'; +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; + +import { v4 as uuid } from 'uuid'; +import copyNonTSFiles, { removeNonTSFiles } from './files'; + +describe('copy', () => { + beforeAll(async () => { + await mkdir(path.join(tmpdir(), 'testcopy'), { recursive: true }); + await mkdir(path.join(tmpdir(), 'testsrc', 'src'), { recursive: true }); + }); + + afterAll(async () => { + await rm(path.join(tmpdir(), 'testcopy'), { recursive: true }); + await rm(path.join(tmpdir(), 'testsrc'), { recursive: true }); + }); + + it('test recursive filtering', async () => { + const rootDirectory = path.join(tmpdir(), 'testcopy', uuid()); + await mkdir(rootDirectory); + + const sourceDirectory = path.join(rootDirectory, 'src'); + const destinationDirectory = path.join(rootDirectory, 'dist'); + await mkdir(destinationDirectory); + + const sourceV1 = path.join(sourceDirectory, 'api', 'v1'); + const sourceV2 = path.join(sourceDirectory, 'api', 'v2'); + + await mkdir(sourceV1, { recursive: true }); + await mkdir(sourceV2, { recursive: true }); + + await writeFile(path.join(sourceV1, 'test.ts'), 'test'); + await writeFile(path.join(sourceV2, 'test2.ts'), 'test'); + await writeFile(path.join(sourceV1, 'actiontestv1.yml'), 'actiontestv1.yml'); + await writeFile(path.join(sourceV2, 'actiontestv2.yml'), 'actiontestv2.yml'); + await writeFile(path.join(sourceDirectory, 'testfile.json'), 'testfile.json'); + await copyNonTSFiles(sourceDirectory, destinationDirectory); + + const file1 = await readFile(path.join(destinationDirectory, 'testfile.json'), 'utf8'); + assert.equal(file1, 'testfile.json'); + const file2 = await readFile(path.join(destinationDirectory, 'api/v1/actiontestv1.yml'), 'utf8'); + assert.equal(file2, 'actiontestv1.yml'); + const file3 = await readFile(path.join(destinationDirectory, 'api/v2/actiontestv2.yml'), 'utf8'); + assert.equal(file3, 'actiontestv2.yml'); + + await assert.rejects(readFile(path.join(destinationDirectory, 'api/v1/test.ts'), 'utf8')); + await assert.rejects(readFile(path.join(destinationDirectory, 'api/v2/test2.ts'), 'utf8')); + }); + + it('test removal of all files except .ts (excluding .spec.ts and .test.ts)', async () => { + const sourceDirectory = path.join(tmpdir(), 'testsrc', 'src'); + await Promise.all([ + writeFile(path.join(sourceDirectory, 'test.ts'), 'test'), + writeFile(path.join(sourceDirectory, 'test.spec.ts'), 'test'), + writeFile(path.join(sourceDirectory, 'swagger.yml'), 'test'), + ]); + await removeNonTSFiles(sourceDirectory); + + const files = await readdir(sourceDirectory, { withFileTypes: true }); + assert.equal(files.length, 2); + assert.ok(files.some((item) => item.name === 'test.ts')); + assert.ok(files.some((item) => item.name === 'swagger.yml')); + }); +}); diff --git a/src/publish-beta/files.ts b/src/publish-beta/files.ts new file mode 100644 index 0000000..4426822 --- /dev/null +++ b/src/publish-beta/files.ts @@ -0,0 +1,37 @@ +// publish-beta/files.ts +import path from 'node:path'; +import fs from 'node:fs/promises'; + +export default async function copyNonTSFiles(sourceDirectory: string, destinationDirectory: string): Promise { + const files = await fs.readdir(sourceDirectory, { withFileTypes: true }); + await Promise.all( + files.map(async (item) => { + const sourceItem = path.join(sourceDirectory, item.name); + const destinationItem = path.join(destinationDirectory, item.name); + if (item.isDirectory()) { + await fs.mkdir(destinationItem, { recursive: true }); + await copyNonTSFiles(sourceItem, path.join(destinationDirectory, item.name)); + return; + } + if (!item.name.endsWith('.ts')) { + await fs.copyFile(sourceItem, destinationItem); + } + }) + ); +} + +export async function removeNonTSFiles(sourceDirectory: string): Promise { + const files = await fs.readdir(sourceDirectory, { withFileTypes: true }); + await Promise.all( + files.map(async (item) => { + const sourceItem = path.join(sourceDirectory, item.name); + if (item.isDirectory()) { + await removeNonTSFiles(sourceItem); + return; + } + if (item.name.endsWith('.ts') && (item.name.endsWith('.spec.ts') || item.name.endsWith('.test.ts'))) { + await fs.rm(sourceItem); + } + }) + ); +} diff --git a/src/publish-beta/github.ts b/src/publish-beta/github.ts index d6f58ad..8230b1d 100644 --- a/src/publish-beta/github.ts +++ b/src/publish-beta/github.ts @@ -9,7 +9,7 @@ export interface GithubConfigurationResponse { number: number; repo: string; } -const log = debug('action:github'); +const log = debug('publish-beta:github'); export function getPRNumber(): string { const prNumberSearch = process.env['GITHUB_REF']?.match(/[0-9]+/gu); diff --git a/src/publish-beta/index.ts b/src/publish-beta/index.ts index 26f4517..b878b1d 100644 --- a/src/publish-beta/index.ts +++ b/src/publish-beta/index.ts @@ -1,17 +1,36 @@ // publish-beta/index.ts + import process from 'node:process'; +import path from 'node:path'; +import { debug } from 'debug'; + +import { publishComment } from './github'; +import { packageJSONUpdate } from './package'; +import copyNonTSFiles from './files'; +import compile from './compile'; +import publish from './publish'; + +const log = debug('publish-beta'); +export async function main(): Promise { + log('Action start'); + + await compile(process.cwd()); + const packageNameAndBetaVersion = await packageJSONUpdate(process.cwd()); + await copyNonTSFiles(path.join(process.cwd(), 'src'), path.join(process.cwd(), 'dist')); + await publish(process.cwd()); + await publishComment(packageNameAndBetaVersion); +} -import command from './command'; -command() +main() .then(() => { process.stdin.destroy(); // eslint-disable-next-line unicorn/no-process-exit process.exit(0); }) // eslint-disable-next-line unicorn/prefer-top-level-await - .catch(() => { + .catch((error) => { // eslint-disable-next-line no-console - console.log('Action Error - exit 1'); + console.log('Action Error - exit 1 - error:', error); // eslint-disable-next-line unicorn/no-process-exit process.exit(1); }); diff --git a/src/publish-beta/package.spec.ts b/src/publish-beta/package.spec.ts index 0a8ef9c..b97b81e 100644 --- a/src/publish-beta/package.spec.ts +++ b/src/publish-beta/package.spec.ts @@ -6,17 +6,19 @@ import path from 'node:path'; import { tmpdir } from 'node:os'; import process from 'node:process'; -import { generatePackageBetaTag, isPackageAnAPI, packageJSONUpdate } from './package'; +import { generatePackageBetaTag, packageJSONUpdate } from './package'; describe('package', () => { beforeAll(async () => { await mkdir(path.join(tmpdir(), 'packageUpdate'), { recursive: true }); + await mkdir(path.join(tmpdir(), 'packageUpdate2'), { recursive: true }); await mkdir(path.join(tmpdir(), 'hasAPI', 'src/api'), { recursive: true }); await mkdir(path.join(tmpdir(), 'noAPI', 'src/'), { recursive: true }); }); afterAll(async () => { await rm(path.join(tmpdir(), 'packageUpdate'), { recursive: true }); + await rm(path.join(tmpdir(), 'packageUpdate2'), { recursive: true }); await rm(path.join(tmpdir(), 'hasAPI'), { recursive: true }); await rm(path.join(tmpdir(), 'noAPI'), { recursive: true }); }); @@ -27,22 +29,30 @@ describe('package', () => { assert.ok(packageBetaTag.startsWith('2406-')); }); - it('hasAPI', async () => { - assert.equal(await isPackageAnAPI(path.join(tmpdir(), 'hasAPI')), true); - }); - it('noAPI', async () => { - assert.equal(await isPackageAnAPI(path.join(tmpdir(), 'noAPI')), false); - }); - - it('packageJSONUpdate', async () => { + it('test packageJSON Update and add /src/ to files', async () => { const filePath = path.join(tmpdir(), 'packageUpdate/package.json'); process.env['GITHUB_REF'] = '/ref/87/branch'; await writeFile( path.join(tmpdir(), 'packageUpdate/package.json'), - JSON.stringify({ name: 'testpackage', version: '1.2.10' }) + JSON.stringify({ name: 'testpackage', version: '1.2.10', files: ['/dist/'] }) ); + await mkdir(path.join(tmpdir(), 'packageUpdate/src'), { recursive: true }); + await packageJSONUpdate(path.join(tmpdir(), 'packageUpdate')); const rawUpdatedFile = await readFile(filePath, 'utf8'); assert.ok(JSON.parse(rawUpdatedFile).version.startsWith('1.2.10-beta.87-')); + assert.deepEqual(JSON.parse(rawUpdatedFile).files.sort(), ['/dist/', '/src/'].sort()); + }); + + it('Test with files property missing', async () => { + process.env['GITHUB_REF'] = '/ref/87/branch'; + await writeFile( + path.join(tmpdir(), 'packageUpdate2/package.json'), + JSON.stringify({ name: 'testpackage', version: '1.2.10' }) + ); + await assert.rejects( + packageJSONUpdate(path.join(tmpdir(), 'packageUpdate2')), + '[Error: package.json does not have a files: [] property]' + ); }); }); diff --git a/src/publish-beta/package.ts b/src/publish-beta/package.ts index 96c66ba..36ef17f 100644 --- a/src/publish-beta/package.ts +++ b/src/publish-beta/package.ts @@ -1,27 +1,19 @@ // publish-beta/package.ts import path from 'node:path'; -import { readFile, stat, writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { debug } from 'debug'; import shortId from './short-id'; import { getPRNumber } from './github'; +import { removeNonTSFiles } from './files'; -const log = debug('action:package'); - -export async function isPackageAnAPI(rootProjectDirectory: string): Promise { - try { - await stat(path.join(rootProjectDirectory, 'src/api')); - log('isAPI:true'); - // eslint-disable-next-line no-console - console.log('::set-output name=IS_API::true'); - return true; - } catch { - log('isAPI:false'); - // eslint-disable-next-line no-console - console.log('::set-output name=IS_API::false'); - } - return false; +const log = debug('publish-beta:package'); + +interface PackageJSON { + name: string; + version: string; + files: string[]; } export function generatePackageBetaTag(): string { @@ -30,14 +22,33 @@ export function generatePackageBetaTag(): string { return `${prNumber}-${id}`; } -export async function packageJSONUpdate(rootProjectDirectory: string): Promise { +function checkFilesPropertyExists(packageJSON: string): void { + const packageJSONObject = JSON.parse(packageJSON) as { files?: string[] }; + if (!packageJSONObject.files) { + throw new Error('package.json does not have a files: [] property'); + } +} + +function addSourceToFilesProperty(input: PackageJSON): string[] { + if (!input.files.includes('/src/')) { + return [...input.files, '/src/']; + } + return input.files; +} + +export async function packageJSONUpdate(rootProjectDirectory: string): Promise { const packageJSONPath = path.join(rootProjectDirectory, 'package.json'); const readPackageJson = await readFile(packageJSONPath, 'utf8'); - const packageJson = JSON.parse(readPackageJson) as { version: string; name: string }; + checkFilesPropertyExists(readPackageJson); + const packageJson = JSON.parse(readPackageJson) as PackageJSON; + + await removeNonTSFiles(path.join(rootProjectDirectory, 'src')); + + const files = addSourceToFilesProperty(packageJson); const newVersion = `${packageJson.version}-beta.${generatePackageBetaTag()}`; packageJson.version = newVersion; + packageJson.files = files; await writeFile(packageJSONPath, JSON.stringify(packageJson)); log(`Updated package.json - new version is: ${packageJson.name}@${newVersion}`); - // eslint-disable-next-line no-console - console.log(`::set-output name=NEW_VERSION::${packageJson.name}@${newVersion}`); + return `${packageJson.name}@${newVersion}`; } diff --git a/src/publish-beta/publish.ts b/src/publish-beta/publish.ts new file mode 100644 index 0000000..b076141 --- /dev/null +++ b/src/publish-beta/publish.ts @@ -0,0 +1,14 @@ +// publish-beta/publish.ts + +import childProcess from 'node:child_process'; +import util from 'node:util'; +import debug from 'debug'; + +const log = debug('publish-beta:publish'); + +const exec = util.promisify(childProcess.exec); +export default async function (directory: string): Promise { + log('publish starting'); + await exec('npm publish --tag beta', { cwd: directory }); + log('publish completed'); +}