@@ -398,6 +398,192 @@ describe('scaleUp with GHES', () => {
398398 10000 ,
399399 ) ;
400400 } ) ;
401+
402+ describe ( 'Dynamic EC2 Configuration' , ( ) => {
403+ beforeEach ( ( ) => {
404+ process . env . ENABLE_ORGANIZATION_RUNNERS = 'true' ;
405+ process . env . ENABLE_DYNAMIC_EC2_CONFIG = 'true' ;
406+ process . env . ENABLE_EPHEMERAL_RUNNERS = 'true' ;
407+ process . env . ENABLE_JOB_QUEUED_CHECK = 'false' ;
408+ process . env . RUNNER_LABELS = 'base-label' ;
409+ process . env . INSTANCE_TYPES = 't3.medium,t3.large' ;
410+ process . env . RUNNER_NAME_PREFIX = 'unit-test' ;
411+ expectedRunnerParams = { ...EXPECTED_RUNNER_PARAMS } ;
412+ mockSSMClient . reset ( ) ;
413+ } ) ;
414+
415+ it ( 'appends EC2 labels to existing runner labels when EC2 labels are present' , async ( ) => {
416+ const testDataWithEc2Labels = [
417+ {
418+ ...TEST_DATA_SINGLE ,
419+ labels : [ 'ghr-ec2-instance-type:c5.2xlarge' , 'ghr-ec2-custom:value' ] ,
420+ messageId : 'test-1' ,
421+ } ,
422+ ] ;
423+
424+ await scaleUpModule . scaleUp ( testDataWithEc2Labels ) ;
425+
426+ // Verify createRunner was called with EC2 instance type extracted from labels
427+ expect ( createRunner ) . toBeCalledWith (
428+ expect . objectContaining ( {
429+ ec2instanceCriteria : expect . objectContaining ( {
430+ instanceTypes : [ 'c5.2xlarge' ] ,
431+ } ) ,
432+ } ) ,
433+ ) ;
434+ } ) ;
435+
436+ it ( 'extracts instance type from EC2 labels and overrides default instance types' , async ( ) => {
437+ const testDataWithEc2Labels = [
438+ {
439+ ...TEST_DATA_SINGLE ,
440+ labels : [ 'ghr-ec2-instance-type:m5.xlarge' , 'ghr-ec2-disk:100' ] ,
441+ messageId : 'test-2' ,
442+ } ,
443+ ] ;
444+
445+ await scaleUpModule . scaleUp ( testDataWithEc2Labels ) ;
446+
447+ expect ( createRunner ) . toBeCalledWith (
448+ expect . objectContaining ( {
449+ ec2instanceCriteria : expect . objectContaining ( {
450+ instanceTypes : [ 'm5.xlarge' ] ,
451+ } ) ,
452+ } ) ,
453+ ) ;
454+ } ) ;
455+
456+ it ( 'uses default instance types when no instance type EC2 label is provided' , async ( ) => {
457+ const testDataWithEc2Labels = [
458+ {
459+ ...TEST_DATA_SINGLE ,
460+ labels : [ 'ghr-ec2-custom:value' ] ,
461+ messageId : 'test-3' ,
462+ } ,
463+ ] ;
464+
465+ await scaleUpModule . scaleUp ( testDataWithEc2Labels ) ;
466+
467+ // Should use the default INSTANCE_TYPES from environment
468+ expect ( createRunner ) . toBeCalledWith (
469+ expect . objectContaining ( {
470+ ec2instanceCriteria : expect . objectContaining ( {
471+ instanceTypes : [ 't3.medium' , 't3.large' ] ,
472+ } ) ,
473+ } ) ,
474+ ) ;
475+ } ) ;
476+
477+ it ( 'does not modify labels when EC2 labels are not present' , async ( ) => {
478+ const testDataWithoutEc2Labels = [
479+ {
480+ ...TEST_DATA_SINGLE ,
481+ labels : [ 'regular-label' , 'another-label' ] ,
482+ messageId : 'test-4' ,
483+ } ,
484+ ] ;
485+
486+ await scaleUpModule . scaleUp ( testDataWithoutEc2Labels ) ;
487+
488+ // Should use default instance types
489+ expect ( createRunner ) . toBeCalledWith (
490+ expect . objectContaining ( {
491+ ec2instanceCriteria : expect . objectContaining ( {
492+ instanceTypes : [ 't3.medium' , 't3.large' ] ,
493+ } ) ,
494+ } ) ,
495+ ) ;
496+ } ) ;
497+
498+ it ( 'handles messages with no labels gracefully' , async ( ) => {
499+ const testDataWithNoLabels = [
500+ {
501+ ...TEST_DATA_SINGLE ,
502+ labels : undefined ,
503+ messageId : 'test-5' ,
504+ } ,
505+ ] ;
506+
507+ await scaleUpModule . scaleUp ( testDataWithNoLabels ) ;
508+
509+ expect ( createRunner ) . toBeCalledWith (
510+ expect . objectContaining ( {
511+ ec2instanceCriteria : expect . objectContaining ( {
512+ instanceTypes : [ 't3.medium' , 't3.large' ] ,
513+ } ) ,
514+ } ) ,
515+ ) ;
516+ } ) ;
517+
518+ it ( 'handles empty labels array' , async ( ) => {
519+ const testDataWithEmptyLabels = [
520+ {
521+ ...TEST_DATA_SINGLE ,
522+ labels : [ ] ,
523+ messageId : 'test-6' ,
524+ } ,
525+ ] ;
526+
527+ await scaleUpModule . scaleUp ( testDataWithEmptyLabels ) ;
528+
529+ expect ( createRunner ) . toBeCalledWith (
530+ expect . objectContaining ( {
531+ ec2instanceCriteria : expect . objectContaining ( {
532+ instanceTypes : [ 't3.medium' , 't3.large' ] ,
533+ } ) ,
534+ } ) ,
535+ ) ;
536+ } ) ;
537+
538+ it ( 'does not process EC2 labels when ENABLE_DYNAMIC_EC2_CONFIG is disabled' , async ( ) => {
539+ process . env . ENABLE_DYNAMIC_EC2_CONFIG = 'false' ;
540+
541+ const testDataWithEc2Labels = [
542+ {
543+ ...TEST_DATA_SINGLE ,
544+ labels : [ 'ghr-ec2-instance-type:c5.4xlarge' ] ,
545+ messageId : 'test-7' ,
546+ } ,
547+ ] ;
548+
549+ await scaleUpModule . scaleUp ( testDataWithEc2Labels ) ;
550+
551+ // Should ignore EC2 labels and use default instance types
552+ expect ( createRunner ) . toBeCalledWith (
553+ expect . objectContaining ( {
554+ ec2instanceCriteria : expect . objectContaining ( {
555+ instanceTypes : [ 't3.medium' , 't3.large' ] ,
556+ } ) ,
557+ } ) ,
558+ ) ;
559+ } ) ;
560+
561+ it ( 'handles multiple EC2 labels correctly' , async ( ) => {
562+ const testDataWithMultipleEc2Labels = [
563+ {
564+ ...TEST_DATA_SINGLE ,
565+ labels : [
566+ 'regular-label' ,
567+ 'ghr-ec2-instance-type:r5.2xlarge' ,
568+ 'ghr-ec2-ami:custom-ami' ,
569+ 'ghr-ec2-disk:200' ,
570+ ] ,
571+ messageId : 'test-8' ,
572+ } ,
573+ ] ;
574+
575+ await scaleUpModule . scaleUp ( testDataWithMultipleEc2Labels ) ;
576+
577+ expect ( createRunner ) . toBeCalledWith (
578+ expect . objectContaining ( {
579+ ec2instanceCriteria : expect . objectContaining ( {
580+ instanceTypes : [ 'r5.2xlarge' ] ,
581+ } ) ,
582+ } ) ,
583+ ) ;
584+ } ) ;
585+ } ) ;
586+
401587 describe ( 'on repo level' , ( ) => {
402588 beforeEach ( ( ) => {
403589 process . env . ENABLE_ORGANIZATION_RUNNERS = 'false' ;
0 commit comments