Skip to content
Closed
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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ jobs:
# Add any additional inputs your action supports
```

The required parameters to deploy are function name, code artifacts directory, handler, and runtime. The function name and code artifacts directory need to be provided by the user. However, the handler and runtime do not and will default to index.handler and nodejs20.x if not provided.
The action automatically updates your Lambda function code when the required parameters are provided. If the function doesn't exist, it will be created first and then updated.

Required parameters include function name, code artifacts directory, handler, and runtime. While function name and code artifacts directory must be specified, handler and runtime will default to `index.handler` and `nodejs20.x` respectively if not provided.

The following examples demonstrate additional features available:

### Update Function Configuration

Expand Down Expand Up @@ -178,7 +182,8 @@ Here's an example of using OIDC with the aws-actions/configure-aws-credentials a
- name: Configure AWS credentials with OIDC
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionRole
role-to-assume: my-role
aws-region: my-region
```

To use OIDC authentication, you must configure a trust policy in AWS IAM that allows GitHub Actions to assume an IAM role. Here's an example trust policy:
Expand Down
157 changes: 157 additions & 0 deletions __tests__/function_create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ jest.mock('@aws-sdk/client-s3', () => {
}))
};
});
jest.mock('@aws-sdk/client-sts', () => {
const original = jest.requireActual('@aws-sdk/client-sts');
return {
...original,
GetCallerIdentityCommand: jest.fn().mockImplementation((params) => ({
...params,
type: 'GetCallerIdentityCommand'
})),
STSClient: jest.fn().mockImplementation(() => ({
send: jest.fn().mockResolvedValue({ Account: '123456789012' })
}))
};
});

afterAll(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -598,4 +611,148 @@ describe('Function Existence Check', () => {
}));
});
});

test('Config changed with dry run logs message', () => {
const configChanged = true;
const dryRun = true;

if (configChanged) {
if (dryRun) {
core.info('[DRY RUN] Configuration updates are not simulated in dry run mode');
return;
}
}

expect(core.info).toHaveBeenCalledWith('[DRY RUN] Configuration updates are not simulated in dry run mode');
});

test('Config changed without dry run calls updateFunctionConfiguration', async () => {
const mockClient = { send: jest.fn() };
const configChanged = true;
const dryRun = false;

if (configChanged) {
if (dryRun) {
core.info('[DRY RUN] Configuration updates are not simulated in dry run mode');
return;
}

await index.updateFunctionConfiguration(mockClient, {
functionName: 'test-function',
role: 'test-role',
handler: 'index.handler',
functionDescription: 'test',
parsedMemorySize: 256,
timeout: 30,
runtime: 'nodejs20.x',
kmsKeyArn: 'test-kms',
ephemeralStorage: 512,
vpcConfig: '{}',
parsedEnvironment: {},
deadLetterConfig: '{}',
tracingConfig: '{}',
layers: '[]',
fileSystemConfigs: '[]',
imageConfig: '{}',
snapStart: '{}',
loggingConfig: '{}',
parsedVpcConfig: {},
parsedDeadLetterConfig: {},
parsedTracingConfig: {},
parsedLayers: [],
parsedFileSystemConfigs: [],
parsedImageConfig: {},
parsedSnapStart: {},
parsedLoggingConfig: {}
});
}

expect(mockClient.send).toHaveBeenCalled();
});

test('No config changes logs no changes message', () => {
const configChanged = false;

if (configChanged) {
// Should not execute
} else {
core.info('No configuration changes detected');
}

expect(core.info).toHaveBeenCalledWith('No configuration changes detected');
});

test('generateS3Key includes commit hash when GITHUB_SHA exists', () => {
process.env.GITHUB_SHA = 'abcdef1234567890';
const result = index.generateS3Key('test-function');
expect(result).toContain('-abcdef1');
delete process.env.GITHUB_SHA;
});

test('validateBucketName validates bucket names', () => {
expect(index.validateBucketName('valid-bucket-name')).toBe(true);
expect(index.validateBucketName('ab')).toBe(false);
expect(index.validateBucketName('INVALID')).toBe(false);
expect(index.validateBucketName('192.168.1.1')).toBe(false);
expect(index.validateBucketName('bucket..name')).toBe(false);
});

test('isEmptyValue checks empty values', () => {
expect(index.isEmptyValue(null)).toBe(true);
expect(index.isEmptyValue('')).toBe(true);
expect(index.isEmptyValue([])).toBe(true);
expect(index.isEmptyValue({})).toBe(true);
expect(index.isEmptyValue('value')).toBe(false);
expect(index.isEmptyValue({ SubnetIds: [] })).toBe(false);
});

test('cleanNullKeys removes null values', () => {
expect(index.cleanNullKeys(null)).toBeUndefined();
expect(index.cleanNullKeys('')).toBeUndefined();
expect(index.cleanNullKeys({ key: 'value', empty: null })).toEqual({ key: 'value' });
expect(index.cleanNullKeys([])).toBeUndefined();
expect(index.cleanNullKeys(['value', null])).toEqual(['value']);
});

test('deepEqual compares objects deeply', () => {
expect(index.deepEqual({ a: 1 }, { a: 1 })).toBe(true);
expect(index.deepEqual({ a: 1 }, { a: 2 })).toBe(false);
expect(index.deepEqual([1, 2], [1, 2])).toBe(true);
expect(index.deepEqual([1, 2], [1, 3])).toBe(false);
expect(index.deepEqual(null, null)).toBe(true);
expect(index.deepEqual('test', 'test')).toBe(true);
});

test('hasConfigurationChanged detects changes', async () => {
const current = { Role: 'old-role', Handler: 'old.handler' };
const updated = { Role: 'new-role', Handler: 'old.handler' };
const result = await index.hasConfigurationChanged(current, updated);
expect(result).toBe(true);
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Configuration difference detected'));
});

test('hasConfigurationChanged returns true for empty current config', async () => {
const result = await index.hasConfigurationChanged({}, { Role: 'test' });
expect(result).toBe(true);
});

test('getAwsAccountId retrieves account ID', async () => {
const mockSend = jest.fn().mockResolvedValue({ Account: '123456789012' });
const { STSClient } = require('@aws-sdk/client-sts');
STSClient.mockImplementation(() => ({ send: mockSend }));

const result = await index.getAwsAccountId('us-east-1');
expect(result).toBe('123456789012');
expect(core.info).toHaveBeenCalledWith('Successfully retrieved AWS account ID: 123456789012');
});

test('getAwsAccountId handles errors', async () => {
const mockSend = jest.fn().mockRejectedValue(new Error('STS error'));
const { STSClient } = require('@aws-sdk/client-sts');
STSClient.mockImplementation(() => ({ send: mockSend }));

const result = await index.getAwsAccountId('us-east-1');
expect(result).toBeNull();
expect(core.warning).toHaveBeenCalledWith('Failed to retrieve AWS account ID: STS error');
});
});
Loading