From 9274303ec1ae9973d8c56fe3bc6582c53864312f Mon Sep 17 00:00:00 2001 From: Humza Mangrio Date: Tue, 13 Jan 2026 11:39:53 -0800 Subject: [PATCH] bugfix: Ensure EBS volumes are deleted upon termination --- README.md | 4 +++- dist/index.js | 35 ++++++++++++++++++++++++++++++++++- src/aws.js | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 16966ffe..fe8fd8aa 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ Now you're ready to go! | `runner-home-dir` | Optional. Used only with the `start` mode. | Specifies a directory where pre-installed actions-runner software and scripts are located.

| | `pre-runner-script` | Optional. Used only with the `start` mode. | Specifies bash commands to run before the runner starts. It's useful for installing dependencies with apt-get, yum, dnf, etc. For example:
          - name: Start EC2 runner
with:
mode: start
...
pre-runner-script: \|
sudo yum update -y && \
sudo yum install docker git libicu -y
sudo systemctl enable docker
| | `market-type` | Optional. Used only with the `start` mode. | This field accepts only the value `spot`.

If set to `spot`, the runner will be launched as a Spot instance.

If omitted, the runner will be launched as an on-demand instance. | -| `block-device-mappings` | Optional. Used only with the `start` mode. | JSON string specifying the block device mappings for the EC2 instance. For example:
[{"DeviceName": "/dev/sda1", "Ebs": {"VolumeSize": 100, "VolumeType": "gp3"}}]
See AWS BlockDeviceMapping docs for all options. | +| `block-device-mappings` | Optional. Used only with the `start` mode. | JSON string specifying the block device mappings for the EC2 instance. For example:
[{"DeviceName": "/dev/sda1", "Ebs": {"VolumeSize": 100, "VolumeType": "gp3"}}]
See AWS BlockDeviceMapping docs for all options.

**Note on EBS Volume Deletion**: All EBS volumes are automatically configured with `DeleteOnTermination: true` to prevent orphaned volumes and unexpected costs. The action will add this property to any volume that doesn't explicitly set it. If you explicitly set `DeleteOnTermination: false`, that setting will be respected. | | `startup-quiet-period-seconds` | Optional | Default: 30 | | `startup-retry-interval-seconds` | Optional | Default: 10 | | `startup-timeout-minutes` | Optional | Default: 5 | @@ -286,6 +286,8 @@ jobs: [ {"DeviceName": "/dev/sda1", "Ebs": {"VolumeSize": 100, "VolumeType": "gp3"}} ] + # Note: To preserve a volume after instance termination, explicitly set DeleteOnTermination: false: + # {"DeviceName": "/dev/sdb", "Ebs": {"VolumeSize": 50, "DeleteOnTermination": false}} do-the-job: name: Do the job on the runner needs: start-runner # required to start the main job when the runner is ready diff --git a/dist/index.js b/dist/index.js index 7e20435b..85a87a9a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -145115,6 +145115,38 @@ function buildMarketOptions() { }; } +/** + * Ensures all block device mappings have DeleteOnTermination set to true. + * Respects explicit user settings if DeleteOnTermination is already defined. + * + * @param {Array} blockDeviceMappings - Array of BlockDeviceMapping objects + * @returns {Array} Modified array with DeleteOnTermination set appropriately + */ +function ensureDeleteOnTermination(blockDeviceMappings) { + if (!Array.isArray(blockDeviceMappings) || blockDeviceMappings.length === 0) { + return blockDeviceMappings; + } + + return blockDeviceMappings.map(mapping => { + // Create a shallow copy to avoid mutating the original + const modifiedMapping = { ...mapping }; + + // Only process if the mapping has an Ebs configuration + if (modifiedMapping.Ebs) { + // Create a shallow copy of the Ebs object + modifiedMapping.Ebs = { ...modifiedMapping.Ebs }; + + // Only set DeleteOnTermination if it's not already explicitly defined + // This respects user's explicit choice if they set it to false + if (modifiedMapping.Ebs.DeleteOnTermination === undefined) { + modifiedMapping.Ebs.DeleteOnTermination = true; + } + } + + return modifiedMapping; + }); +} + async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, label, githubRegistrationToken, region) { // Region is always specified now, so we can directly use it const ec2ClientOptions = { region }; @@ -145143,13 +145175,14 @@ async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, l Ebs: { ...(config.input.ec2VolumeSize !== '' && { VolumeSize: config.input.ec2VolumeSize }), ...(config.input.ec2VolumeType !== '' && { VolumeType: config.input.ec2VolumeType }), + DeleteOnTermination: true, }, }, ]; } if (config.input.blockDeviceMappings.length > 0) { - params.BlockDeviceMappings = config.input.blockDeviceMappings; + params.BlockDeviceMappings = ensureDeleteOnTermination(config.input.blockDeviceMappings); } const result = await ec2.send(new RunInstancesCommand(params)); diff --git a/src/aws.js b/src/aws.js index 80afacfa..afe5a29c 100644 --- a/src/aws.js +++ b/src/aws.js @@ -107,6 +107,38 @@ function buildMarketOptions() { }; } +/** + * Ensures all block device mappings have DeleteOnTermination set to true. + * Respects explicit user settings if DeleteOnTermination is already defined. + * + * @param {Array} blockDeviceMappings - Array of BlockDeviceMapping objects + * @returns {Array} Modified array with DeleteOnTermination set appropriately + */ +function ensureDeleteOnTermination(blockDeviceMappings) { + if (!Array.isArray(blockDeviceMappings) || blockDeviceMappings.length === 0) { + return blockDeviceMappings; + } + + return blockDeviceMappings.map(mapping => { + // Create a shallow copy to avoid mutating the original + const modifiedMapping = { ...mapping }; + + // Only process if the mapping has an Ebs configuration + if (modifiedMapping.Ebs) { + // Create a shallow copy of the Ebs object + modifiedMapping.Ebs = { ...modifiedMapping.Ebs }; + + // Only set DeleteOnTermination if it's not already explicitly defined + // This respects user's explicit choice if they set it to false + if (modifiedMapping.Ebs.DeleteOnTermination === undefined) { + modifiedMapping.Ebs.DeleteOnTermination = true; + } + } + + return modifiedMapping; + }); +} + async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, label, githubRegistrationToken, region) { // Region is always specified now, so we can directly use it const ec2ClientOptions = { region }; @@ -135,13 +167,14 @@ async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, l Ebs: { ...(config.input.ec2VolumeSize !== '' && { VolumeSize: config.input.ec2VolumeSize }), ...(config.input.ec2VolumeType !== '' && { VolumeType: config.input.ec2VolumeType }), + DeleteOnTermination: true, }, }, ]; } if (config.input.blockDeviceMappings.length > 0) { - params.BlockDeviceMappings = config.input.blockDeviceMappings; + params.BlockDeviceMappings = ensureDeleteOnTermination(config.input.blockDeviceMappings); } const result = await ec2.send(new RunInstancesCommand(params));