├── Diff_runTests_pullrequest.yml ├── Diff_runTests_push.yml ├── README.md ├── SECURITY.md ├── diffGitHub_pullrequest.m ├── diffGitHub_push.m ├── githubrunner_pullrequest.yml ├── githubrunner_push.yml └── license.txt /Diff_runTests_pullrequest.yml: -------------------------------------------------------------------------------- 1 | 2 | name: PullRequest - Diff to Ancestor, Publish and attach Report, and Run Project Tests 3 | 4 | # Controls when the workflow will run 5 | on: 6 | # Triggers the workflow on pull request events but only for the main branch 7 | 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | my-job: 13 | name: Diff to Ancestor, Publish and attach Report, and Run Project Tests 14 | runs-on: self-hosted 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Compare Models to Ancestors 21 | uses: matlab-actions/run-command@v2 22 | with: 23 | command: branch ="${{ github.head_ref }}", diffGitHub_pullrequest(branch) 24 | - name: Upload Comparison Reports 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: diffreports 28 | path: ${{ github.workspace }}\*.html 29 | - name: Run All Project Tests 30 | uses: matlab-actions/run-command@v2 31 | with: 32 | command: runtests 33 | 34 | # Copyright 2022-2024 The MathWorks, Inc. 35 | -------------------------------------------------------------------------------- /Diff_runTests_push.yml: -------------------------------------------------------------------------------- 1 | 2 | name: PushToMain - Diff to Ancestor, Publish and attach Report, and Run Project Tests 3 | 4 | # Controls when the workflow will run 5 | on: 6 | # Triggers the workflow on push events but only for the main branch 7 | 8 | push: 9 | branches: [ main ] 10 | 11 | jobs: 12 | my-job: 13 | name: Diff to Ancestor, Publish and attach Report, and Run Project Tests 14 | runs-on: self-hosted 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Compare Models to Ancestors 21 | uses: matlab-actions/run-command@v2 22 | with: 23 | command: lastpush="${{ github.event.before }}", diffGitHub_push(lastpush) 24 | - name: Upload Comparison Reports 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: diffreports 28 | path: ${{ github.workspace }}\*.html 29 | - name: Run All Project Tests 30 | uses: matlab-actions/run-command@v2 31 | with: 32 | command: runtests 33 | 34 | # Copyright 2022-2024 The MathWorks, Inc. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simulink Model Comparison for GitHub Pull Requests 2 | 3 | 4 | Automate the generation of Simulink model diffs for GitHub® pull requests and push events using GitHub actions. Automatically attach the comparison reports to the pull request or push event for easy viewing outside of MATLAB® and Simulink®. 5 | 6 | [![Open in MATLAB Online](https://www.mathworks.com/images/responsive/global/open-in-matlab-online.svg)](https://matlab.mathworks.com/open/github/v1?repo=mathworks/Simulink-Model-Comparison-for-GitHub-Pull-Requests) 7 | 8 | ## Prerequisites 9 | 1. The GitHub actions, defined in the Diff_runTests_push.yml and Diff_runTests_pullrequest.yml files, use a self-hosted runner. To add a self-hosted runner to your repository, see https://docs.github.com/en/actions/hosting-your-own-runners. 10 | Using a self-hosted runner enables you to keep your repository private. 11 | 2. Ensure that you have MATLAB and Simulink installed on the self-hosted runner you are using. See https://www.mathworks.com. 12 | 13 | Alternatively, if you prefer to use a GitHub-hosted runner, use the actions defined in the githubrunner_push.yml and githubrunner_pullrequest.yml files instead. 14 | 15 | ## Setup 16 | To set up the workflows on GitHub: 17 | 1. Download the .m files and .yml files. 18 | 2. Add the .m files to your repository. 19 | 3. Add the .yml files to the .github/workflows folder in your repository. 20 | 21 | To see the setup and the workflow in action, watch https://www.youtube.com/watch?v=PNX7f0kPSYg. 22 | 23 | Visit https://www.mathworks.com/help/simulink/ug/model-diff-pull-requests.html. 24 | 25 | ## Details 26 | This repository provides two .yml files and two .m files. 27 | The .yml files set up workflows to be triggered on push and pull request on GitHub. 28 | The workflows use the .m files to: 29 | 1) Get the list of modified model files and their ancestors using Git™ commands. 30 | 3) Compare every modified model to its ancestor and publish the comparison report. 31 | 2) Upload all model comparison reports to the job when it is complete. 32 | 3) Run all project tests. 33 | 34 | ## Notes 35 | 1) Taking screenshots to include in the comparison report requires your runner to have a display. 36 | On Linux, if your runner does not have a display, you can use one of the following workarounds in your YAML files: 37 | - Start a display server before the "Compare Models to Ancestors" step. For an example, see the "Start Display Server" step in the githubrunner_pullrequest.yml file. 38 | - Use xvfb-run to run commands on a display server implementing the X11 display server protocol. 39 | For example, in your .yml file, use: 40 | ```yaml 41 | - name: Compare Models to Ancestors 42 | run: xvfb-run path-to-matlab/bin/matlab -batch "branch ='${{ github.head_ref }}'; diffGitHub_pullrequest(branch)" 43 | ``` 44 | 45 | 2) Starting in R2022b, the Comparison Tool allows you to generate comparison reports with no screenshots when you run jobs on a no-display machine. 46 | 47 | ## License 48 | The license is available in the License file within this repository. 49 | 50 | Copyright (c) 2022-2023, The MathWorks, Inc. 51 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | If you believe you have discovered a security vulnerability, please report it to 4 | [security@mathworks.com](mailto:security@mathworks.com). Please see 5 | [MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html) 6 | for additional information. -------------------------------------------------------------------------------- /diffGitHub_pullrequest.m: -------------------------------------------------------------------------------- 1 | function diffGitHub_pullrequest(branchname) 2 | % Open project 3 | proj = openProject(pwd); 4 | 5 | % List modified models since branch diverged from main 6 | % Use *** to search recursively for modified SLX files starting in the current folder 7 | % git diff --name-only main..branchtomerge 8 | gitCommand = sprintf('git --no-pager diff --name-only origin/main..origin/%s ***.slx', branchname); 9 | [status,modifiedFiles] = system(gitCommand); 10 | if status ~= 0 11 | warning("git diff failed") 12 | warning(modifiedFiles) 13 | return; 14 | end 15 | modifiedFiles = split(modifiedFiles); 16 | modifiedFiles(end) = []; % Removing last element because it is empty 17 | 18 | if isempty(modifiedFiles) 19 | disp('No modified models to compare.') 20 | return 21 | end 22 | 23 | % Create a temporary folder to store the ancestors of the modified models 24 | % If you have models with the same name in different folders, consider 25 | % creating multiple folders to prevent overwriting temporary models 26 | tempdir = fullfile(proj.RootFolder, "modelscopy"); 27 | mkdir(tempdir) 28 | 29 | % Generate a comparison report for every modified model file 30 | for i = 1:numel(modifiedFiles) 31 | diffToAncestor(tempdir,string(modifiedFiles(i))); 32 | end 33 | 34 | % Delete the temporary folder 35 | rmdir modelscopy s 36 | end 37 | 38 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 39 | 40 | function report = diffToAncestor(tempdir,fileName) 41 | ancestor = getAncestor(tempdir,fileName); 42 | if isempty(ancestor) 43 | % new model - skip diff report 44 | report = []; 45 | return 46 | end 47 | 48 | % Compare models and publish results in a printable report 49 | % Specify the format using 'pdf', 'html', or 'docx' 50 | comp= visdiff(ancestor, fileName); 51 | filter(comp, 'unfiltered'); 52 | report = publish(comp,'html'); 53 | 54 | end 55 | 56 | 57 | function ancestor = getAncestor(tempdir,fileName) 58 | 59 | [~, name, ext] = fileparts(fileName); 60 | ancestor = fullfile(tempdir, name); 61 | 62 | % Replace seperators to work with Git and create ancestor file name 63 | fileName = strrep(fileName, '\', '/'); 64 | ancestor = strrep(sprintf('%s%s%s',ancestor, "_ancestor", ext), '\', '/'); 65 | % Build git command to get ancestor from main 66 | % git show origin/main:models/modelname.slx > modelscopy/modelname_ancestor.slx 67 | gitCommand = sprintf('git --no-pager show origin/main:%s > %s', fileName, ancestor); 68 | 69 | [status, ~] = system(gitCommand); 70 | if status ~= 0 71 | % new model 72 | ancestor = []; 73 | end 74 | end 75 | 76 | % Copyright 2024-2025 The MathWorks, Inc. 77 | -------------------------------------------------------------------------------- /diffGitHub_push.m: -------------------------------------------------------------------------------- 1 | function diffGitHub_push(lastpush) 2 | % Open project 3 | proj = openProject(pwd); 4 | 5 | % List modified models since the last push. Use *** to search recursively for modified 6 | % SLX files starting in the current folder 7 | % git diff --name-only lastpush ***.slx 8 | gitCommand = sprintf('git --no-pager diff --name-only %s ***.slx', lastpush); 9 | [status,modifiedFiles] = system(gitCommand); 10 | if status ~= 0 11 | warning("git diff failed") 12 | warning(modifiedFiles) 13 | return; 14 | end 15 | modifiedFiles = split(modifiedFiles); 16 | modifiedFiles(end) = []; % Removing last element because it is empty 17 | 18 | if isempty(modifiedFiles) 19 | disp('No modified models to compare.') 20 | return 21 | end 22 | 23 | % Create a temporary folder to store the ancestors of the modified models 24 | % If you have models with the same name in different folders, consider 25 | % creating multiple folders to prevent overwriting temporary models 26 | tempdir = fullfile(proj.RootFolder, "modelscopy"); 27 | mkdir(tempdir) 28 | 29 | % Generate a comparison report for every modified model file 30 | for i = 1:numel(modifiedFiles) 31 | diffToAncestor(tempdir,string(modifiedFiles(i)),lastpush); 32 | end 33 | 34 | % Delete the temporary folder 35 | rmdir modelscopy s 36 | end 37 | 38 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 39 | 40 | function report = diffToAncestor(tempdir,fileName,lastpush) 41 | 42 | ancestor = getAncestor(tempdir,fileName,lastpush); 43 | if isempty(ancestor) 44 | % new model - skip diff report 45 | report = []; 46 | return 47 | end 48 | 49 | % Compare models and publish results in a printable report 50 | % Specify the format using 'pdf', 'html', or 'docx' 51 | comp= visdiff(ancestor, fileName); 52 | filter(comp, 'unfiltered'); 53 | report = publish(comp,'html'); 54 | 55 | end 56 | 57 | 58 | function ancestor = getAncestor(tempdir,fileName,lastpush) 59 | 60 | [~, name, ext] = fileparts(fileName); 61 | ancestor = fullfile(tempdir, name); 62 | 63 | % Replace seperators to work with Git and create ancestor file name 64 | fileName = strrep(fileName, '\', '/'); 65 | ancestor = strrep(sprintf('%s%s%s',ancestor, "_ancestor", ext), '\', '/'); 66 | 67 | % Build git command to get ancestor 68 | % git show lastpush:models/modelname.slx > modelscopy/modelname_ancestor.slx 69 | gitCommand = sprintf('git --no-pager show %s:%s > %s', lastpush, fileName, ancestor); 70 | 71 | [status, ~] = system(gitCommand); 72 | if status ~= 0 73 | % new model 74 | ancestor = []; 75 | end 76 | end 77 | 78 | % Copyright 2023-2025 The MathWorks, Inc. -------------------------------------------------------------------------------- /githubrunner_pullrequest.yml: -------------------------------------------------------------------------------- 1 | 2 | name: PullRequest GitHub Runner - Diff to Ancestor, Publish and attach Report, and Run Project Tests 3 | 4 | # Controls when the workflow will run 5 | on: 6 | # Triggers the workflow on pull request events but only for the main branch 7 | 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | my-job: 13 | name: Diff to Ancestor, Publish and attach Report, and Run Project Tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Set up MATLAB 21 | uses: matlab-actions/setup-matlab@v2 22 | - name: Start display server 23 | run: | 24 | sudo apt-get install -y xvfb 25 | Xvfb :99 & 26 | export DISPLAY=:99 27 | echo "DISPLAY=:99" >> $GITHUB_ENV 28 | - name: Compare Models to Ancestors 29 | uses: matlab-actions/run-command@v1 30 | with: 31 | command: branch ="${{ github.head_ref }}", diffGitHub_pullrequest(branch) 32 | - name: Upload Comparison Reports 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: diffreports 36 | path: ${{ github.workspace }}/*.html 37 | - name: Run All Project Tests 38 | uses: matlab-actions/run-command@v2 39 | with: 40 | command: runtests 41 | 42 | # Copyright 2022-2024 The MathWorks, Inc. 43 | -------------------------------------------------------------------------------- /githubrunner_push.yml: -------------------------------------------------------------------------------- 1 | 2 | name: PushToMain - Diff to Ancestor, Publish and attach Report, and Run Project Tests 3 | 4 | # Controls when the workflow will run 5 | on: 6 | # Triggers the workflow on push events but only for the main branch 7 | 8 | push: 9 | branches: [ main ] 10 | 11 | jobs: 12 | my-job: 13 | name: Diff to Ancestor, Publish and attach Report, and Run Project Tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Set up MATLAB 21 | uses: matlab-actions/setup-matlab@v2 22 | - name: Start Display Server 23 | run: | 24 | sudo apt-get install -y xvfb 25 | Xvfb :99 & 26 | export DISPLAY=:99 27 | echo "DISPLAY=:99" >> $GITHUB_ENV 28 | - name: Compare Models to Ancestors 29 | uses: matlab-actions/run-command@v2 30 | with: 31 | command: lastpush="${{ github.event.before }}", diffGitHub_push(lastpush) 32 | - name: Upload Comparison Reports 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: diffreports 36 | path: ${{ github.workspace }}/*.html 37 | - name: Run All Project Tests 38 | uses: matlab-actions/run-command@v2 39 | with: 40 | command: runtests 41 | 42 | # Copyright 2022-2024 The MathWorks, Inc. 43 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, The MathWorks, Inc. 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 5 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 6 | 3. In all cases, the software is, and all modifications and derivatives of the software shall be, licensed to you solely for use in conjunction with MathWorks products and service offerings. 7 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------