@@ -774,25 +774,30 @@ func TestCallWorkflowOnly_UsesHandlerManagerStep(t *testing.T) {
774774// TestCreateCodeScanningAlertUploadJob verifies that when create-code-scanning-alert is configured,
775775// a dedicated upload_code_scanning_sarif job is created (separate from safe_outputs) and that
776776// the safe_outputs job:
777- // - exports sarif_file and checkout_token outputs for the upload job
777+ // - exports sarif_file output for the upload job
778778// - uploads the SARIF file as a GitHub Actions artifact so the upload job
779779// (which runs in a fresh workspace) can download it
780+ //
781+ // Token handling: the upload job computes tokens directly (static PAT or minted GitHub App token)
782+ // rather than reading from safe_outputs job outputs, because GitHub Actions masks secret references
783+ // in job outputs — "Skip output 'x' since it may contain secret".
780784func TestCreateCodeScanningAlertUploadJob (t * testing.T ) {
781785 tests := []struct {
782786 name string
783787 config * CreateCodeScanningAlertsConfig
788+ checkoutConfigs []* CheckoutConfig
784789 expectUploadJob bool
785- expectCustomToken string
786- expectTokenFromOutputs bool // expect needs.safe_outputs.outputs.checkout_token
790+ expectTokenInSteps string // expected token expression in upload job steps
791+ expectAppTokenMintStep bool // expect a GitHub App token minting step in upload job
787792 safeOutputsGitHubToken string
788793 }{
789794 {
790- name : "default config creates separate upload job using checkout_token from outputs " ,
795+ name : "default config creates separate upload job with static token computed directly " ,
791796 config : & CreateCodeScanningAlertsConfig {
792797 BaseSafeOutputConfig : BaseSafeOutputConfig {},
793798 },
794- expectUploadJob : true ,
795- expectTokenFromOutputs : true ,
799+ expectUploadJob : true ,
800+ expectTokenInSteps : "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" ,
796801 },
797802 {
798803 name : "custom per-config github-token is used in upload step token" ,
@@ -801,20 +806,48 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) {
801806 GitHubToken : "${{ secrets.GHAS_TOKEN }}" ,
802807 },
803808 },
804- expectUploadJob : true ,
805- expectCustomToken : "${{ secrets.GHAS_TOKEN }}" ,
806- expectTokenFromOutputs : true ,
809+ expectUploadJob : true ,
810+ expectTokenInSteps : "${{ secrets.GHAS_TOKEN }}" ,
807811 },
808812 {
809813 name : "safe-outputs-level github-token is used in upload step token" ,
810814 config : & CreateCodeScanningAlertsConfig {
811815 BaseSafeOutputConfig : BaseSafeOutputConfig {},
812816 },
813817 expectUploadJob : true ,
814- expectCustomToken : "${{ secrets.SO_TOKEN }}" ,
815- expectTokenFromOutputs : true ,
818+ expectTokenInSteps : "${{ secrets.SO_TOKEN }}" ,
816819 safeOutputsGitHubToken : "${{ secrets.SO_TOKEN }}" ,
817820 },
821+ {
822+ name : "checkout with github-app mints a fresh app token in the upload job" ,
823+ config : & CreateCodeScanningAlertsConfig {
824+ BaseSafeOutputConfig : BaseSafeOutputConfig {},
825+ },
826+ checkoutConfigs : []* CheckoutConfig {
827+ {
828+ GitHubApp : & GitHubAppConfig {
829+ AppID : "${{ vars.APP_ID }}" ,
830+ PrivateKey : "${{ secrets.APP_PRIVATE_KEY }}" ,
831+ },
832+ },
833+ },
834+ expectUploadJob : true ,
835+ expectTokenInSteps : "${{ steps.checkout-restore-app-token.outputs.token }}" ,
836+ expectAppTokenMintStep : true ,
837+ },
838+ {
839+ name : "checkout with github-token PAT uses that PAT directly in upload job" ,
840+ config : & CreateCodeScanningAlertsConfig {
841+ BaseSafeOutputConfig : BaseSafeOutputConfig {},
842+ },
843+ checkoutConfigs : []* CheckoutConfig {
844+ {
845+ GitHubToken : "${{ secrets.MY_CHECKOUT_PAT }}" ,
846+ },
847+ },
848+ expectUploadJob : true ,
849+ expectTokenInSteps : "${{ secrets.MY_CHECKOUT_PAT }}" ,
850+ },
818851 {
819852 name : "staged mode does not create upload job" ,
820853 config : & CreateCodeScanningAlertsConfig {
@@ -837,9 +870,10 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) {
837870 CreateCodeScanningAlerts : tt .config ,
838871 GitHubToken : tt .safeOutputsGitHubToken ,
839872 },
873+ CheckoutConfigs : tt .checkoutConfigs ,
840874 }
841875
842- // 1. Verify safe_outputs job exports sarif_file and checkout_token, and uploads artifact
876+ // 1. Verify safe_outputs job exports sarif_file and uploads the artifact
843877 safeOutputsJob , _ , err := compiler .buildConsolidatedSafeOutputsJob (workflowData , string (constants .AgentJobName ), "test-workflow.md" )
844878 require .NoError (t , err , "safe_outputs job should build without error" )
845879 require .NotNil (t , safeOutputsJob , "safe_outputs job should be generated" )
@@ -853,9 +887,10 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) {
853887 assert .Contains (t , safeOutputsJob .Outputs ["sarif_file" ], "steps.process_safe_outputs.outputs.sarif_file" ,
854888 "sarif_file output must reference process_safe_outputs step" )
855889
856- // safe_outputs must export checkout_token for the upload job to use
857- assert .Contains (t , safeOutputsJob .Outputs , "checkout_token" ,
858- "safe_outputs job must export checkout_token output for the upload job" )
890+ // safe_outputs must NOT export checkout_token — GitHub Actions masks secret
891+ // references in job outputs, making them arrive empty in downstream jobs.
892+ assert .NotContains (t , safeOutputsJob .Outputs , "checkout_token" ,
893+ "safe_outputs job must NOT export checkout_token (secret refs are masked in job outputs)" )
859894
860895 // safe_outputs must upload the SARIF file as an artifact so the upload job
861896 // (running in a fresh workspace) can download it
@@ -888,6 +923,11 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) {
888923
889924 uploadSteps := strings .Join (uploadJob .Steps , "" )
890925
926+ // The upload job must NOT use needs.safe_outputs.outputs.checkout_token — it
927+ // would arrive empty because GitHub Actions masks secret refs in job outputs.
928+ assert .NotContains (t , uploadSteps , "needs.safe_outputs.outputs.checkout_token" ,
929+ "Upload job must NOT read checkout_token from safe_outputs outputs (would be masked)" )
930+
891931 // Restore checkout step must be present in the upload job
892932 assert .Contains (t , uploadSteps , "Restore checkout to triggering commit" ,
893933 "Upload job must restore workspace to triggering commit" )
@@ -898,9 +938,17 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) {
898938 assert .NotContains (t , uploadSteps , "git checkout ${{ github.sha }}" ,
899939 "Must use actions/checkout, not a raw git command" )
900940
901- // The restore checkout step must always use the checkout_token from safe_outputs outputs
902- assert .Contains (t , uploadSteps , "needs.safe_outputs.outputs.checkout_token" ,
903- "Restore checkout step must use checkout_token from safe_outputs outputs" )
941+ if tt .expectAppTokenMintStep {
942+ // GitHub App checkout: a token minting step must appear before the restore checkout
943+ assert .Contains (t , uploadSteps , "checkout-restore-app-token" ,
944+ "Upload job must mint a GitHub App token before restoring checkout" )
945+ mintPos := strings .Index (uploadSteps , "checkout-restore-app-token" )
946+ restoreCheckoutPos := strings .Index (uploadSteps , "Restore checkout to triggering commit" )
947+ require .NotEqual (t , - 1 , mintPos , "App token minting step must be present in upload job steps" )
948+ require .NotEqual (t , - 1 , restoreCheckoutPos , "Restore checkout step must be present in upload job steps" )
949+ assert .Less (t , mintPos , restoreCheckoutPos ,
950+ "App token minting step must appear before the restore checkout step" )
951+ }
904952
905953 // Download SARIF artifact step must be present in the upload job
906954 assert .Contains (t , uploadSteps , "Download SARIF artifact" ,
@@ -946,16 +994,13 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) {
946994 assert .Less (t , downloadPos , uploadPos ,
947995 "SARIF download must appear before SARIF upload in the job steps" )
948996
949- if tt .expectCustomToken != "" {
950- assert .Contains (t , uploadSteps , tt .expectCustomToken ,
951- "Upload SARIF token must use the per-config or safe-outputs github-token" )
952- }
953- if tt .expectTokenFromOutputs {
954- assert .Contains (t , uploadSteps , "needs.safe_outputs.outputs.checkout_token" ,
955- "Upload job should use checkout_token from safe_outputs outputs" )
997+ // Verify the expected token expression appears in the upload job steps
998+ if tt .expectTokenInSteps != "" {
999+ assert .Contains (t , uploadSteps , tt .expectTokenInSteps ,
1000+ "Upload job must use the expected token in its steps" )
9561001 }
9571002 } else {
958- // staged: safe_outputs should NOT export sarif_file or checkout_token
1003+ // staged: safe_outputs should NOT export sarif_file
9591004 assert .NotContains (t , safeOutputsJob .Outputs , "sarif_file" ,
9601005 "staged mode: safe_outputs must not export sarif_file" )
9611006 assert .NotContains (t , safeOutputsJob .Outputs , "checkout_token" ,
0 commit comments