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