@@ -619,6 +619,190 @@ def create_release(is_draft:, assets: [], name: nil)
619619 end
620620 end
621621
622+ describe '#upload_release_assets' do
623+ let ( :test_repo ) { 'repo-test/project-test' }
624+ let ( :test_version ) { '1.0.0' }
625+ let ( :release_url ) { 'https://api.github.com/repos/repo-test/project-test/releases/123' }
626+ let ( :release_html_url ) { 'https://github.com/repo-test/project-test/releases/tag/1.0.0' }
627+ let ( :release ) { sawyer_resource_stub ( url : release_url , html_url : release_html_url , tag_name : test_version ) }
628+ let ( :existing_assets ) { [ ] }
629+ let ( :uploaded_asset ) { release_asset ( name : 'test-app.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/999' ) }
630+ let ( :client ) do
631+ instance_double (
632+ Octokit ::Client ,
633+ user : instance_double ( 'User' , name : 'test' ) ,
634+ 'auto_paginate=' : nil
635+ )
636+ end
637+ let ( :helper ) do
638+ described_class . new ( github_token : 'Fake-GitHubToken-123' )
639+ end
640+
641+ before do
642+ allow ( Octokit ::Client ) . to receive ( :new ) . and_return ( client )
643+ allow ( client ) . to receive ( :releases ) . with ( test_repo ) . and_return ( [ release ] )
644+ allow ( client ) . to receive ( :release_assets ) . with ( release_url ) . and_return ( existing_assets )
645+ allow ( client ) . to receive_messages ( upload_asset : uploaded_asset , delete_release_asset : true )
646+ end
647+
648+ it 'fails clearly if the release does not exist' do
649+ allow ( client ) . to receive ( :releases ) . with ( test_repo ) . and_return ( [ ] )
650+
651+ with_tmp_file ( named : 'test-app.zip' ) do |file_path |
652+ expect do
653+ upload_release_assets ( assets : [ file_path ] )
654+ end . to raise_error ( FastlaneCore ::Interface ::FastlaneError , "Could not find GitHub Release for tag #{ test_version } in #{ test_repo } " )
655+ end
656+ end
657+
658+ it 'fails clearly if an asset file does not exist' do
659+ expect ( client ) . not_to receive ( :releases )
660+ expect ( client ) . not_to receive ( :release_assets )
661+ expect ( client ) . not_to receive ( :upload_asset )
662+
663+ expect do
664+ upload_release_assets ( assets : [ 'missing-file.zip' ] )
665+ end . to raise_error ( FastlaneCore ::Interface ::FastlaneError , "Can't find file missing-file.zip!" )
666+ end
667+
668+ it 'fails clearly if an asset is not a file path' do
669+ expect ( client ) . not_to receive ( :releases )
670+ expect ( client ) . not_to receive ( :release_assets )
671+ expect ( client ) . not_to receive ( :upload_asset )
672+
673+ expect do
674+ upload_release_assets ( assets : [ 123 ] )
675+ end . to raise_error ( FastlaneCore ::Interface ::FastlaneError , 'release_assets must contain file paths' )
676+ end
677+
678+ it 'fails without mutating GitHub when local assets have duplicate filenames' do
679+ in_tmp_dir do |tmpdir |
680+ first_dir = File . join ( tmpdir , 'ios' )
681+ second_dir = File . join ( tmpdir , 'tvos' )
682+ Dir . mkdir ( first_dir )
683+ Dir . mkdir ( second_dir )
684+
685+ first_file_path = File . join ( first_dir , 'test-app.zip' )
686+ second_file_path = File . join ( second_dir , 'test-app.zip' )
687+ File . write ( first_file_path , 'ios' )
688+ File . write ( second_file_path , 'tvos' )
689+
690+ expect ( client ) . not_to receive ( :releases )
691+ expect ( client ) . not_to receive ( :release_assets )
692+ expect ( client ) . not_to receive ( :delete_release_asset )
693+ expect ( client ) . not_to receive ( :upload_asset )
694+
695+ expect do
696+ upload_release_assets ( assets : [ first_file_path , second_file_path ] , replace_existing : false )
697+ end . to raise_error ( FastlaneCore ::Interface ::FastlaneError , 'release_assets must not contain duplicate filenames' )
698+ end
699+ end
700+
701+ it 'uploads assets to a draft release' do
702+ draft_release = sawyer_resource_stub ( url : release_url , html_url : release_html_url , tag_name : test_version , draft : true )
703+ other_release = sawyer_resource_stub ( url : 'https://api.github.com/repos/repo-test/project-test/releases/456' , html_url : 'https://github.com/repo-test/project-test/releases/tag/0.9.0' , tag_name : '0.9.0' )
704+
705+ allow ( client ) . to receive ( :releases ) . with ( test_repo ) . and_return ( [ other_release , draft_release ] )
706+ allow ( client ) . to receive ( :release_assets ) . with ( release_url ) . and_return ( [ ] )
707+
708+ with_tmp_file ( named : 'test-app.zip' ) do |file_path |
709+ expect ( client ) . to receive ( :upload_asset ) . with ( release_url , file_path , { content_type : 'application/octet-stream' } )
710+
711+ result = upload_release_assets ( assets : [ file_path ] )
712+
713+ expect ( result ) . to eq ( release_html_url )
714+ end
715+ end
716+
717+ it 'uploads one asset to the existing release' do
718+ with_tmp_file ( named : 'test-app.zip' ) do |file_path |
719+ expect ( client ) . to receive ( :upload_asset ) . with ( release_url , file_path , { content_type : 'application/octet-stream' } )
720+
721+ result = upload_release_assets ( assets : [ file_path ] )
722+
723+ expect ( result ) . to eq ( release_html_url )
724+ end
725+ end
726+
727+ it 'uploads multiple assets to the existing release' do
728+ in_tmp_dir do |tmpdir |
729+ first_file_path = File . join ( tmpdir , 'test-ios.zip' )
730+ second_file_path = File . join ( tmpdir , 'test-tvos.zip' )
731+ File . write ( first_file_path , 'ios' )
732+ File . write ( second_file_path , 'tvos' )
733+
734+ first_uploaded_asset = release_asset ( name : 'test-ios.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/1000' )
735+ second_uploaded_asset = release_asset ( name : 'test-tvos.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/1001' )
736+
737+ expect ( client ) . to receive ( :upload_asset ) . with ( release_url , first_file_path , { content_type : 'application/octet-stream' } ) . ordered . and_return ( first_uploaded_asset )
738+ expect ( client ) . to receive ( :upload_asset ) . with ( release_url , second_file_path , { content_type : 'application/octet-stream' } ) . ordered . and_return ( second_uploaded_asset )
739+
740+ result = upload_release_assets ( assets : [ first_file_path , second_file_path ] )
741+
742+ expect ( result ) . to eq ( release_html_url )
743+ end
744+ end
745+
746+ it 'replaces an existing asset with the same filename' do
747+ existing_asset = release_asset ( name : 'test-app.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/1234' )
748+ allow ( client ) . to receive ( :release_assets ) . with ( release_url ) . and_return ( [ existing_asset ] )
749+
750+ with_tmp_file ( named : 'test-app.zip' ) do |file_path |
751+ expect ( client ) . to receive ( :delete_release_asset ) . with ( existing_asset . url )
752+ expect ( client ) . to receive ( :upload_asset ) . with ( release_url , file_path , { content_type : 'application/octet-stream' } )
753+
754+ result = upload_release_assets ( assets : [ file_path ] )
755+
756+ expect ( result ) . to eq ( release_html_url )
757+ end
758+ end
759+
760+ it 'preserves unrelated existing assets' do
761+ matching_asset = release_asset ( name : 'test-app.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/1234' )
762+ unrelated_asset = release_asset ( name : 'other-platform.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/5678' )
763+ deleted_asset_urls = [ ]
764+
765+ allow ( client ) . to receive ( :release_assets ) . with ( release_url ) . and_return ( [ matching_asset , unrelated_asset ] )
766+ allow ( client ) . to receive ( :delete_release_asset ) do |asset_url |
767+ deleted_asset_urls << asset_url
768+ true
769+ end
770+
771+ with_tmp_file ( named : 'test-app.zip' ) do |file_path |
772+ upload_release_assets ( assets : [ file_path ] )
773+ end
774+
775+ expect ( deleted_asset_urls ) . to eq ( [ matching_asset . url ] )
776+ end
777+
778+ it 'fails without deleting or uploading when replace_existing is false and a matching asset exists' do
779+ existing_asset = release_asset ( name : 'test-app.zip' , url : 'https://api.github.com/repos/repo-test/project-test/releases/assets/1234' )
780+ allow ( client ) . to receive ( :release_assets ) . with ( release_url ) . and_return ( [ existing_asset ] )
781+
782+ expect ( client ) . not_to receive ( :delete_release_asset )
783+ expect ( client ) . not_to receive ( :upload_asset )
784+
785+ with_tmp_file ( named : 'test-app.zip' ) do |file_path |
786+ expect do
787+ upload_release_assets ( assets : [ file_path ] , replace_existing : false )
788+ end . to raise_error ( FastlaneCore ::Interface ::FastlaneError , "GitHub Release #{ test_version } already has an asset named test-app.zip. Set replace_existing: true to replace it." )
789+ end
790+ end
791+
792+ def upload_release_assets ( assets :, replace_existing : true )
793+ helper . upload_release_assets (
794+ repository : test_repo ,
795+ version : test_version ,
796+ assets : assets ,
797+ replace_existing : replace_existing
798+ )
799+ end
800+
801+ def release_asset ( name :, url :)
802+ sawyer_resource_stub ( name : name , url : url )
803+ end
804+ end
805+
622806 describe '#github_token_config_item' do
623807 it 'has the correct key' do
624808 expect ( described_class . github_token_config_item . key ) . to eq ( :github_token )
0 commit comments