diff --git a/package-lock.json b/package-lock.json index aa29bcb..c207b19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@checkdigit/github-actions", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/github-actions", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", diff --git a/package.json b/package.json index 4d6e469..6fde5b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/github-actions", - "version": "2.1.0", + "version": "2.2.0", "description": " Provides supporting operations for github action builds.", "author": "Check Digit, LLC", "license": "MIT", diff --git a/src/check-label/check-label-compare-match-semver.spec.ts b/src/check-label/check-label-compare-match-semver.spec.ts index 2c2edec..d89d4c1 100644 --- a/src/check-label/check-label-compare-match-semver.spec.ts +++ b/src/check-label/check-label-compare-match-semver.spec.ts @@ -6,7 +6,7 @@ import { describe, it } from '@jest/globals'; import { validateVersion } from './check-label'; -const assertError = 'Version is incorrect based on Pull Request label'; +const assertError = /Version is incorrect based on Pull Request label/u; // expected error message when version is incorrect - assert adds additional information to error, so regex is used describe('compare and match semver', () => { it('Test basic patch', async () => { diff --git a/src/publish-beta/validate-name-and-resource-length.spec.ts b/src/publish-beta/validate-name-and-resource-length.spec.ts new file mode 100644 index 0000000..f11b2a6 --- /dev/null +++ b/src/publish-beta/validate-name-and-resource-length.spec.ts @@ -0,0 +1,211 @@ +// publish-beta/validate-name-and-resource-length.spec.ts + +import { strict as assert } from 'node:assert'; +import { describe, it } from '@jest/globals'; + +import { type PackageJSON, validateNameAndResourceLength } from './validate-name-and-resource-length'; + +describe('Test name and resource length', () => { + it('No services property', async () => { + const packageJSON: PackageJSON = {}; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('No resources property', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('No aws resources property', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: {}, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('Empty aws resources property', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: { + aws: {}, + }, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('Empty S3 resources property', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: { + aws: { + s3: {}, + }, + }, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('Name all within valid length', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: { + aws: { + s3: { + 'valid-s3-name': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'valid-s3-name', + }, + }, + }, + }, + }, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('Service name too long', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestNameThatIsTooLong1', + resources: { + aws: { + s3: { + 'valid-s3-name': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'valid-s3-name', + }, + }, + }, + }, + }, + }, + }; + await assert.rejects(validateNameAndResourceLength(packageJSON)); + }); + + it('Service name too long - exempt service', async () => { + process.env['SERVICE_NAME_LENGTH_EXCEPTION'] = 'teampay-vendor-management'; + const packageJSON: PackageJSON = { + service: { + name: 'teampay-vendor-management', + resources: { + aws: { + s3: { + 'valid-s3-name': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'valid-s3-bucket-name', + }, + }, + }, + }, + }, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('S3 bucket name too long', async () => { + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: { + aws: { + s3: { + bucket1: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'valid name', + }, + }, + bucket2: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'invalid-bucket-length', + }, + }, + }, + }, + }, + }, + }; + await assert.rejects(validateNameAndResourceLength(packageJSON)); + }); + + it('S3 bucket name too long - exempt bucket', async () => { + process.env['S3_BUCKET_NAME_LENGTH_EXCEPTIONS'] = 'ach.teampay.armor.inbound'; + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: { + aws: { + s3: { + bucket1: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'valid name', + }, + }, + 'ach.teampay.armor.inbound': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'ach.teampay.armor.inbound', + }, + }, + }, + }, + }, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); + + it('S3 bucket name too long - multiple exempt bucket', async () => { + process.env['S3_BUCKET_NAME_LENGTH_EXCEPTIONS'] = 'mastercard.armor.inbound,ach.teampay.armor.inbound'; + const packageJSON: PackageJSON = { + service: { + name: 'TestName', + resources: { + aws: { + s3: { + bucket1: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'valid name', + }, + }, + 'ach.teampay.armor.inbound': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'ach.teampay.armor.inbound', + }, + }, + 'mastercard.armor.inbound': { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: 'mastercard.armor.inbound', + }, + }, + }, + }, + }, + }, + }; + await assert.doesNotReject(validateNameAndResourceLength(packageJSON)); + }); +}); diff --git a/src/publish-beta/validate-name-and-resource-length.ts b/src/publish-beta/validate-name-and-resource-length.ts new file mode 100644 index 0000000..1f66eb9 --- /dev/null +++ b/src/publish-beta/validate-name-and-resource-length.ts @@ -0,0 +1,94 @@ +// publish-beta/validate-name-and-resource-length.ts + +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; + +import debug from 'debug'; + +const log = debug('github-actions:publish-beta:validate-names'); + +interface S3Properties { + Type: 'AWS::S3::Bucket'; + Properties: { + BucketName: string; + }; +} + +interface Resources { + aws?: { + s3?: Record; + }; +} + +export interface PackageJSON { + service?: { + name: string; + resources?: Resources; + }; +} + +const MAXIMUM_SERVICE_NAME_LENGTH = 20; + +const MAXIMUM_S3_BUCKET_NAME_LENGTH = 20; + +export async function readPackageJSON(rootProjectDirectory: string): Promise { + const packageJSONPath = path.join(rootProjectDirectory, 'package.json'); + const packageJSON = await readFile(packageJSONPath, 'utf8'); + return JSON.parse(packageJSON) as PackageJSON; +} + +async function validateS3BucketNames(input: Resources) { + if (input.aws?.s3 === undefined) { + log('package.json does not have a service.resources.aws.s3: {} property'); + return; + } + // allow override of s3 bucket name length from action environment + const listOfS3BucketsFromEnvironment = process.env['S3_BUCKET_NAME_LENGTH_EXCEPTIONS'] ?? undefined; + const S3_BUCKET_NAME_LENGTH_EXCEPTIONS = + listOfS3BucketsFromEnvironment === undefined ? new Set() : new Set(listOfS3BucketsFromEnvironment.split(',')); + + const s3Resources = input.aws.s3; + + const bucketNames = Object.values(s3Resources) + .map((resource) => resource.Properties.BucketName) + .filter((name) => !S3_BUCKET_NAME_LENGTH_EXCEPTIONS.has(name)) + .filter((name) => name.length > MAXIMUM_S3_BUCKET_NAME_LENGTH); + + if (bucketNames.length > 0) { + throw new Error( + `S3 bucket names are longer than ${MAXIMUM_S3_BUCKET_NAME_LENGTH} characters: ${JSON.stringify(bucketNames)}`, + ); + } +} + +export async function validateNameAndResourceLength(packageJSONWithResources: PackageJSON): Promise { + if (!packageJSONWithResources.service) { + log('package.json does not have a service: {} property'); + return; + } + // allow override of service name length from action environment + const SERVICE_NAME_LENGTH_EXCEPTION = process.env['SERVICE_NAME_LENGTH_EXCEPTION'] ?? undefined; + const serviceName = packageJSONWithResources.service.name; + + if (SERVICE_NAME_LENGTH_EXCEPTION !== serviceName && serviceName.length > MAXIMUM_SERVICE_NAME_LENGTH) { + const message = `Service name ${serviceName} is longer than ${MAXIMUM_SERVICE_NAME_LENGTH} characters`; + log(message); + throw new Error(message); + } + + if (!packageJSONWithResources.service.resources?.aws) { + log('package.json does not have a service.resources.aws: {} property'); + return; + } + const resources = packageJSONWithResources.service.resources; + await validateS3BucketNames(resources); +} + +export default async function (): Promise { + log('Action start'); + + const packageJSONWithResources = await readPackageJSON(process.cwd()); + await validateNameAndResourceLength(packageJSONWithResources); + + log('Action end'); +} diff --git a/src/validate-npm-package/validate-npm-package.spec.ts b/src/validate-npm-package/validate-npm-package.spec.ts index 4ba94ae..3171784 100644 --- a/src/validate-npm-package/validate-npm-package.spec.ts +++ b/src/validate-npm-package/validate-npm-package.spec.ts @@ -25,6 +25,10 @@ describe('validate-npm-package', () => { await verifyNpmPackage(); }, 300_000); + // Test uses a bad version of approval package + // and requires skipLibCheck: false in tsconfig.json + // we set it manually in validate npm package as + // checkdigit/typescript-config is various versions of this setting it('bad npm package results in error', async () => { actionsCoreSpy.mockImplementationOnce((name) => { if (name === 'betaPackage') { diff --git a/src/validate-npm-package/validate-npm-package.ts b/src/validate-npm-package/validate-npm-package.ts index e66502a..09c49e1 100644 --- a/src/validate-npm-package/validate-npm-package.ts +++ b/src/validate-npm-package/validate-npm-package.ts @@ -63,6 +63,9 @@ async function generateProject(workFolder: string, packageJson: PackageJson): Pr // create tsconfig.json const tsconfigJson = { extends: '@checkdigit/typescript-config', + compilerOptions: { + skipLibCheck: false, + }, }; await fs.writeFile(`${workFolder}/tsconfig.json`, JSON.stringify(tsconfigJson, null, 2)); }