Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check Service and Resource name maximum length #81

Merged
merged 7 commits into from
Feb 10, 2025
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/check-label/check-label-compare-match-semver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
211 changes: 211 additions & 0 deletions src/publish-beta/validate-name-and-resource-length.spec.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
94 changes: 94 additions & 0 deletions src/publish-beta/validate-name-and-resource-length.ts
Original file line number Diff line number Diff line change
@@ -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<string, S3Properties>;
};
}

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<PackageJSON> {
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<void> {
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<void> {
log('Action start');

const packageJSONWithResources = await readPackageJSON(process.cwd());
await validateNameAndResourceLength(packageJSONWithResources);

log('Action end');
}
4 changes: 4 additions & 0 deletions src/validate-npm-package/validate-npm-package.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
3 changes: 3 additions & 0 deletions src/validate-npm-package/validate-npm-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Loading