diff --git a/.github/actions/build-test-matrix/action.yml b/.github/actions/build-test-matrix/action.yml new file mode 100644 index 0000000..a404dbb --- /dev/null +++ b/.github/actions/build-test-matrix/action.yml @@ -0,0 +1,243 @@ +name: 'Build Test Matrix' +description: 'Determine MATLAB versions from toolbox info and build test matrix with Python versions' + +inputs: + tools_directory: + description: 'Directory containing MLToolboxInfo.json' + required: false + default: 'tools' + matlab_versions: + description: 'MATLAB versions to test (overrides auto-detection from MLToolboxInfo.json)' + required: false + default: '[]' + python_versions: + description: 'JSON object mapping MATLAB versions to Python versions (overrides for specific versions only)' + required: false + default: '{}' + include_python: + description: 'Whether to include Python versions in the test matrix' + required: false + default: 'true' + +outputs: + matrix: + description: 'Test matrix with MATLAB and Python versions' + value: ${{ steps.build.outputs.matrix }} + matlab_versions: + description: 'Determined MATLAB versions array' + value: ${{ steps.determine.outputs.matlab_versions }} + +runs: + using: "composite" + steps: + - name: Determine MATLAB versions from toolbox info + id: determine + shell: bash + run: | + # Load configuration from config.json + config_file="${{ github.action_path }}/config.json" + config_min_release=$(jq -r '.minimumMatlabRelease' "$config_file") + config_max_release=$(jq -r '.maximumMatlabRelease' "$config_file") + + # Get the latest MATLAB release from MathWorks API (this is our ceiling) + latest_matlab_release=$(curl -s "https://ssd.mathworks.com/supportfiles/ci/matlab-release/v0/latest") + echo "Latest MATLAB release from MathWorks API: $latest_matlab_release" + + # Read min and max MATLAB releases from MLToolboxInfo.json + min_release_raw=$(jq -r '.ToolboxOptions.MinimumMatlabRelease' "${{ inputs.tools_directory }}/MLToolboxInfo.json") + max_release_raw=$(jq -r '.ToolboxOptions.MaximumMatlabRelease' "${{ inputs.tools_directory }}/MLToolboxInfo.json") + + # Determine minimum release (use config default if not specified or if older than config minimum) + min_release="$config_min_release" + if [ "$min_release_raw" != "null" ] && [ "$min_release_raw" != "" ]; then + # Compare releases to ensure we don't go below config minimum + min_year=${min_release_raw:1:4} + min_letter=${min_release_raw:5:1} + config_min_year=${config_min_release:1:4} + config_min_letter=${config_min_release:5:1} + + # If the specified minimum is newer than config minimum, use it + if [ $min_year -gt $config_min_year ] || ([ $min_year -eq $config_min_year ] && [ "$min_letter" \> "$config_min_letter" ]); then + min_release="$min_release_raw" + fi + fi + + # Determine maximum release (use latest from API as ceiling) + max_release="$latest_matlab_release" + if [ "$max_release_raw" != "null" ] && [ "$max_release_raw" != "" ]; then + # Compare with latest release to enforce ceiling + max_year=${max_release_raw:1:4} + max_letter=${max_release_raw:5:1} + latest_year=${latest_matlab_release:1:4} + latest_letter=${latest_matlab_release:5:1} + + # If the specified maximum is older than latest, use the specified one + if [ $max_year -lt $latest_year ] || ([ $max_year -eq $latest_year ] && [ "$max_letter" \< "$latest_letter" ]); then + max_release="$max_release_raw" + fi + fi + + echo "Min release from MLToolboxInfo: $min_release_raw" + echo "Min release for GitHub Actions: $min_release" + echo "Max release from MLToolboxInfo: $max_release_raw" + echo "Max release for GitHub Actions: $max_release (capped at $latest_matlab_release)" + + # Generate MATLAB releases between min and max + generate_matlab_releases() { + local min_release=$1 + local max_release=$2 + + # Extract year and letter from releases (e.g., R2023a -> 2023, a) + min_year=${min_release:1:4} + min_letter=${min_release:5:1} + max_year=${max_release:1:4} + max_letter=${max_release:5:1} + + releases="" + + for year in $(seq $min_year $max_year); do + if [ $year -eq $min_year ] && [ $year -eq $max_year ]; then + # Same year - use range from min to max letter + start_letter=$min_letter + end_letter=$max_letter + elif [ $year -eq $min_year ]; then + # First year - start from min letter, go to 'b' + start_letter=$min_letter + end_letter="b" + elif [ $year -eq $max_year ]; then + # Last year - start from 'a', go to max letter + start_letter="a" + end_letter=$max_letter + else + # Middle years - include both 'a' and 'b' + start_letter="a" + end_letter="b" + fi + + # Add releases for this year + if [ "$start_letter" = "a" ]; then + if [ -n "$releases" ]; then releases="$releases,"; fi + releases="$releases\"R${year}a\"" + if [ "$end_letter" = "b" ]; then + releases="$releases,\"R${year}b\"" + fi + elif [ "$start_letter" = "b" ]; then + if [ -n "$releases" ]; then releases="$releases,"; fi + releases="$releases\"R${year}b\"" + fi + done + + echo "[$releases]" + } + + matlab_versions=$(generate_matlab_releases "$min_release" "$max_release") + echo "Generated MATLAB versions: $matlab_versions" + echo "matlab_versions=$matlab_versions" >> $GITHUB_OUTPUT + + - name: Build test matrix + id: build + shell: bash + run: | + # Check if input MATLAB versions are provided + input_matlab_versions='${{ inputs.matlab_versions }}' + if [ "$input_matlab_versions" != "" ] && [ "$input_matlab_versions" != "null" ] && [ "$input_matlab_versions" != "[]" ]; then + # Use input MATLAB versions but filter them by config min/max limits + config_file="${{ github.action_path }}/config.json" + config_min_release=$(jq -r '.minimumMatlabRelease' "$config_file") + + # Get the latest MATLAB release from MathWorks API (this is our ceiling) + latest_matlab_release=$(curl -s "https://ssd.mathworks.com/supportfiles/ci/matlab-release/v0/latest") + + # Function to compare MATLAB releases + is_version_in_range() { + local version=$1 + local min_release=$2 + local max_release=$3 + + # Extract year and letter from version + version_year=${version:1:4} + version_letter=${version:5:1} + + # Extract min release components + min_year=${min_release:1:4} + min_letter=${min_release:5:1} + + # Extract max release components + max_year=${max_release:1:4} + max_letter=${max_release:5:1} + + # Check if version is >= min_release + if [ $version_year -lt $min_year ] || ([ $version_year -eq $min_year ] && [ "$version_letter" \< "$min_letter" ]); then + return 1 + fi + + # Check if version is <= max_release + if [ $version_year -gt $max_year ] || ([ $version_year -eq $max_year ] && [ "$version_letter" \> "$max_letter" ]); then + return 1 + fi + + return 0 + } + + # Filter input versions to only include those within config limits + filtered_versions="" + for version in $(echo $input_matlab_versions | jq -r '.[]'); do + if is_version_in_range "$version" "$config_min_release" "$latest_matlab_release"; then + if [ -n "$filtered_versions" ]; then + filtered_versions="$filtered_versions," + fi + filtered_versions="$filtered_versions\"$version\"" + else + echo "Excluding $version (outside config range $config_min_release to $latest_matlab_release)" + fi + done + + matlab_versions="[$filtered_versions]" + echo "Using input MATLAB versions (filtered by config limits): $matlab_versions" + else + # Use auto-determined MATLAB versions from previous step + matlab_versions='${{ steps.determine.outputs.matlab_versions }}' + echo "Using auto-determined MATLAB versions: $matlab_versions" + fi + + # Check if Python should be included in the matrix + include_python='${{ inputs.include_python }}' + + if [ "$include_python" = "true" ]; then + # Load Python versions from config file + config_file="${{ github.action_path }}/config.json" + config_python_versions=$(jq -c '.pythonVersions' "$config_file") + + # Merge input python_versions overrides with config defaults + input_python_versions='${{ inputs.python_versions }}' + if [ "$input_python_versions" != "" ] && [ "$input_python_versions" != "null" ] && [ "$input_python_versions" != "{}" ]; then + # Merge input overrides with config defaults + python_versions=$(echo "$config_python_versions $input_python_versions" | jq -s '.[0] * .[1]') + echo "Using config Python versions with input overrides: $python_versions" + else + python_versions="$config_python_versions" + echo "Using config Python versions: $python_versions" + fi + + # Build the matrix include section dynamically + include_items="" + for version in $(echo $matlab_versions | jq -r '.[]'); do + python_version=$(echo $python_versions | jq -r --arg v "$version" '.[$v]') + if [ "$python_version" != "null" ]; then + if [ -n "$include_items" ]; then + include_items="$include_items," + fi + include_items="$include_items{\"MATLABVersion\":\"$version\",\"pythonVersion\":\"$python_version\"}" + fi + done + + # Create the full matrix with Python versions + matrix="{\"MATLABVersion\":$matlab_versions,\"include\":[$include_items]}" + else + # Create matrix without Python versions + matrix="{\"MATLABVersion\":$matlab_versions}" + echo "Python versions excluded from matrix" + fi + + echo "Generated matrix: $matrix" + echo "matrix=$matrix" >> $GITHUB_OUTPUT diff --git a/.github/actions/build-test-matrix/config.json b/.github/actions/build-test-matrix/config.json new file mode 100644 index 0000000..1a99a97 --- /dev/null +++ b/.github/actions/build-test-matrix/config.json @@ -0,0 +1,15 @@ +{ + "minimumMatlabRelease": "R2021a", + "maximumMatlabRelease": "latest", + "pythonVersions": { + "R2021a": "3.8", + "R2021b": "3.9", + "R2022a": "3.9", + "R2022b": "3.10", + "R2023a": "3.10", + "R2023b": "3.11", + "R2024a": "3.11", + "R2024b": "3.12", + "R2025a": "3.12" + } +} diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml new file mode 100644 index 0000000..4f02e23 --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,64 @@ +name: 'Create GitHub Release' +description: 'Create a GitHub release with packaged toolbox' + +inputs: + version_number: + description: 'Version number for the release' + required: true + mltbx_path: + description: 'Path to the MLTBX file' + required: true + +runs: + using: "composite" + steps: + - name: Commit updated Contents.m file + shell: bash + continue-on-error: true + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git status + git add code/Contents.m + git commit -m "Final check-ins for release v${{ inputs.version_number }} [skip-ci]" + git fetch + git push + + - name: Update tag + shell: bash + if: always() + continue-on-error: true + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Delete the existing tag locally if it exists + if git tag -l "v${{ inputs.version_number }}" | grep -q "v${{ inputs.version_number }}"; then + echo "Deleting existing local tag v${{ inputs.version_number }}" + git tag -d "v${{ inputs.version_number }}" + else + echo "Local tag v${{ inputs.version_number }} does not exist" + fi + + # Delete the existing tag remotely if it exists + if git ls-remote --tags origin | grep -q "refs/tags/v${{ inputs.version_number }}"; then + echo "Deleting existing remote tag v${{ inputs.version_number }}" + git push origin --delete "v${{ inputs.version_number }}" + else + echo "Remote tag v${{ inputs.version_number }} does not exist" + fi + + # Create the tag with a message, including [skip ci] to prevent CI workflows + git tag -a "v${{ inputs.version_number }}" -m "Release v${{ inputs.version_number }} [skip ci]" + + # Push the new tag to the remote repository + git push origin "v${{ inputs.version_number }}" + + - name: Create GitHub release + uses: ncipollo/release-action@v1 + with: + draft: true + artifacts: "${{ inputs.mltbx_path }}" + tag: "v${{ inputs.version_number }}" + generateReleaseNotes: true + body: "![MATLAB Versions Tested](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2F${{ github.repository }}%2Fgh-badges%2F.github%2Fbadges%2Fv${{ inputs.version_number }}%2Ftested_with.json)" diff --git a/.github/actions/install-matbox/installMatBox.m b/.github/actions/install-matbox/installMatBox.m index 68223be..d328cb6 100644 --- a/.github/actions/install-matbox/installMatBox.m +++ b/.github/actions/install-matbox/installMatBox.m @@ -38,8 +38,10 @@ function installFromCommit() scriptPath = mfilename('fullpath'); projectFolder = extractBefore(scriptPath, fullfile('.github', 'actions')); + + % We only want to add the code folder to MATLAB's path here to avoid + % potential path conflicts with task functions in the tools folder. codeDirectory = fullfile(projectFolder, 'code'); - addpath(genpath(codeDirectory)) savepath() end diff --git a/.github/actions/package-toolbox/action.yml b/.github/actions/package-toolbox/action.yml new file mode 100644 index 0000000..8e8c913 --- /dev/null +++ b/.github/actions/package-toolbox/action.yml @@ -0,0 +1,52 @@ +name: 'Package MATLAB Toolbox' +description: 'Package a MATLAB toolbox with specified version' + +inputs: + version_number: + description: 'Version number to use for packaging' + required: true + code_directory: + description: 'Directory containing the MATLAB code to package' + required: false + default: 'code' + tools_directory: + description: 'Directory containing packaging tools' + required: false + default: 'tools' + +outputs: + mltbx_path: + description: 'Path to the packaged MLTBX file' + value: ${{ steps.set_version.outputs.mltbx_path }} + toolbox_name: + description: 'Name of the toolbox from MLToolboxInfo.json' + value: ${{ steps.set_version.outputs.toolbox_name }} + +runs: + using: "composite" + steps: + - name: Package toolbox + id: package + uses: matlab-actions/run-command@v2 + with: + command: | + addpath(genpath("${{ inputs.tools_directory }}")); + versionNumberStr = "v${{ inputs.version_number }}"; + if exist("packageToolbox", "file") + [~, mltbxPath] = packageToolbox("specific", versionNumberStr); + else + [~, mltbxPath] = matbox.tasks.packageToolbox(pwd, "specific", versionNumberStr, ... + 'SourceFolderName', "${{ inputs.code_directory }}"); + end + + % Write the MLTBX path to a file for the next step + matbox.utility.filewrite('mltbx_path.txt', mltbxPath); + + - name: Set version number + id: set_version + shell: bash + run: | + toolboxName=$(jq -r '.ToolboxOptions.ToolboxName' "${{ inputs.tools_directory }}/MLToolboxInfo.json") + mltbxPath=$(cat mltbx_path.txt) + echo "toolbox_name=$toolboxName" >> $GITHUB_OUTPUT + echo "mltbx_path=$mltbxPath" >> $GITHUB_OUTPUT diff --git a/.github/actions/push-badges/action.yml b/.github/actions/push-badges/action.yml index 67df8b9..d4c72c4 100644 --- a/.github/actions/push-badges/action.yml +++ b/.github/actions/push-badges/action.yml @@ -12,33 +12,33 @@ inputs: runs: using: "composite" steps: - # Check out the actual source branch in a separate working tree - - name: Checkout PR branch for pushing badges - uses: actions/checkout@v4 - with: - ref: ${{ inputs.pr-ref }} - repository: ${{ inputs.pr-repo }} - path: pr-branch + # Check out the actual source branch in a separate working tree + - name: Checkout PR branch for pushing badges + uses: actions/checkout@v4 + with: + ref: ${{ inputs.pr-ref }} + repository: ${{ inputs.pr-repo }} + path: pr-branch - # Copy generated files to actual branch - - name: Copy badges to PR branch - shell: bash - run: | - cp -r .github/badges/* pr-branch/.github/badges/ 2>/dev/null || echo "No badges to copy" + # Copy generated files to actual branch + - name: Copy badges to PR branch + shell: bash + run: | + cp -r .github/badges/* pr-branch/.github/badges/ 2>/dev/null || echo "No badges to copy" - # Commit updated SVG badges for the issues and tests (if changed) - - name: Commit and push SVG badges if updated - working-directory: pr-branch - shell: bash - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch + # Commit updated SVG badges for the issues and tests (if changed) + - name: Commit and push SVG badges if updated + working-directory: pr-branch + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch - if [[ $(git add .github/badges/* --dry-run | wc -l) -gt 0 ]]; then - git add .github/badges - git commit -m "Update GitHub badges" - git push origin HEAD - else - echo "Nothing to commit" - fi + if [[ $(git add .github/badges/* --dry-run | wc -l) -gt 0 ]]; then + git add .github/badges + git commit -m "Update GitHub badges" + git push origin HEAD + else + echo "Nothing to commit" + fi diff --git a/.github/actions/test-code/action.yml b/.github/actions/test-code/action.yml index 9e60b7d..90ca733 100644 --- a/.github/actions/test-code/action.yml +++ b/.github/actions/test-code/action.yml @@ -10,6 +10,14 @@ inputs: tools_directory: description: 'Where the testToolbox function is located' default: './tools' + report_subdirectory: + description: 'Subdirectory for test reports' + required: false + default: '' + create_badge: + description: 'Whether to create a badge' + required: false + default: 'true' runs: using: "composite" @@ -21,15 +29,20 @@ runs: command: | addpath(genpath("${{ inputs.tools_directory }}")); if exist("testToolbox", "file") - testToolbox(); + testToolbox(... + "ReportSubdirectory", "${{ inputs.report_subdirectory }}", ... + "CreateBadge", strcmp(${{ inputs.create_badge }}, 'true')); else matbox.tasks.testToolbox(pwd, ... "SourceFolderName", "${{ inputs.code_directory }}", ... - "ToolsFolderName", "${{ inputs.tools_directory }}"); + "ToolsFolderName", "${{ inputs.tools_directory }}", ... + "ReportSubdirectory", "${{ inputs.report_subdirectory }}", ... + "CreateBadge", strcmp(${{ inputs.create_badge }}, 'true')); end - name: Publish test results uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: - files: "docs/reports/test-results.xml" + check_name: ${{ inputs.report_subdirectory != '' && format('Test Results ({0})', inputs.report_subdirectory) || 'Test Results' }} + files: "docs/reports/**/test-results.xml" diff --git a/.github/actions/update-badges/action.yml b/.github/actions/update-badges/action.yml new file mode 100644 index 0000000..82b23fa --- /dev/null +++ b/.github/actions/update-badges/action.yml @@ -0,0 +1,93 @@ +# Should only be used in a job where MATLAB and MatBox is installed in previous steps + +name: 'Update "tested with" badge' +description: 'Generate the "tested with" badge for a release and push to `gh-badges` branch' + +inputs: + version_number: + description: 'Version number for the release' + required: true + tools_directory: + description: 'Directory containing badge tools' + required: false + default: 'tools' + +runs: + using: "composite" + steps: + - name: Download test reports + uses: actions/download-artifact@v4 + with: + pattern: reports-* + path: docs/reports + merge-multiple: true + + - name: Generate tested with badge + uses: matlab-actions/run-command@v2 + with: + command: | + addpath(genpath("${{ inputs.tools_directory }}")); + versionNumberStr = "v${{ inputs.version_number }}"; + if exist("createTestedWithBadgeforToolbox", "file") + createTestedWithBadgeforToolbox(versionNumberStr); + else + matbox.tasks.createTestedWithBadgeforToolbox(versionNumberStr, pwd); + end + + - name: Check if gh-badges branch exists + id: check_branch + shell: bash + run: | + if git ls-remote --heads origin gh-badges | grep -q gh-badges; then + echo "branch_exists=true" >> $GITHUB_OUTPUT + echo "gh-badges branch exists" + else + echo "branch_exists=false" >> $GITHUB_OUTPUT + echo "gh-badges branch does not exist" + fi + + - name: Checkout existing gh-badges branch + if: steps.check_branch.outputs.branch_exists == 'true' + uses: actions/checkout@v4 + with: + ref: gh-badges + path: gh-badges + + - name: Create gh-badges branch if it doesn't exist + if: steps.check_branch.outputs.branch_exists == 'false' + shell: bash + run: | + echo "Creating gh-badges branch..." + mkdir -p gh-badges + cd gh-badges + git init + git remote add origin https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git + git checkout --orphan gh-badges + + # Copy initialization files + cp ${{ github.action_path }}/gh-badges-init/* . + + git add . + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "Initialize gh-badges branch" + git push origin gh-badges + + - name: Push to gh-badges + shell: bash + run: | + mkdir -p gh-badges/.github/badges/v${{ inputs.version_number }} + cp .github/badges/v${{ inputs.version_number }}/tested_with.json gh-badges/.github/badges/v${{ inputs.version_number }}/tested_with.json + cd gh-badges + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Only proceed with commit and push if changes are detected + if [[ $(git add .github/badges/* --dry-run | wc -l) -gt 0 ]]; then + git add .github/badges/* + git commit -m "Update tested with badge for release" + git push origin gh-badges + else + echo "Nothing to commit" + fi diff --git a/.github/actions/update-badges/gh-badges-init/.gitignore b/.github/actions/update-badges/gh-badges-init/.gitignore new file mode 100644 index 0000000..379e122 --- /dev/null +++ b/.github/actions/update-badges/gh-badges-init/.gitignore @@ -0,0 +1,7 @@ +* +!.gitignore +!README.md +!.github +!.github/badges +!.github/badges/* +!.github/badges/**/* diff --git a/.github/actions/update-badges/gh-badges-init/README.md b/.github/actions/update-badges/gh-badges-init/README.md new file mode 100644 index 0000000..4dd23a0 --- /dev/null +++ b/.github/actions/update-badges/gh-badges-init/README.md @@ -0,0 +1,3 @@ +# GitHub Badges + +This branch is reserved for storing badges for the GitHub releases page diff --git a/.github/actions/validate-version/action.yml b/.github/actions/validate-version/action.yml new file mode 100644 index 0000000..f7d7a9a --- /dev/null +++ b/.github/actions/validate-version/action.yml @@ -0,0 +1,53 @@ +name: 'Validate Version' +description: 'Validate version number format from tag or input' + +inputs: + version: + description: 'Version number (for manual triggers)' + required: false + ref_name: + description: 'GitHub ref name (for tag triggers)' + required: false + +outputs: + version_number: + description: 'Validated version number (without v prefix)' + value: ${{ steps.set_version.outputs.version_number }} + +runs: + using: "composite" + steps: + - name: Check for retag + if: ${{ contains(github.event.head_commit.message, '[skip-ci]') }} + shell: bash + run: | + echo "Error: Commit message contains [skip-ci], skipping." + exit 1 + + - name: Set version based on trigger type + id: set_version + shell: bash + run: | + if [[ -n "${{ inputs.version }}" ]]; then + # For manual trigger, use the input version + VERSION_NUMBER="${{ inputs.version }}" + # Validate format + if [[ ! "$VERSION_NUMBER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Input for 'version' ('$VERSION_NUMBER') is not in the expected major.minor.patch format." + exit 1 + fi + else + # For tag trigger, use the tag name + TAG_NAME="${{ inputs.ref_name }}" + # Remove 'v' prefix if present for the version number + VERSION_NUMBER="${TAG_NAME#v}" + + # Validate format + if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Tag name ('$TAG_NAME') is not in the expected v*.*.* format." + exit 1 + fi + fi + + echo "Using version number: $VERSION_NUMBER" + echo "version_number=$VERSION_NUMBER" >> $GITHUB_OUTPUT diff --git a/.github/workflow-templates/prepare-release-modular.properties.json b/.github/workflow-templates/prepare-release-modular.properties.json new file mode 100644 index 0000000..9a2ce27 --- /dev/null +++ b/.github/workflow-templates/prepare-release-modular.properties.json @@ -0,0 +1,10 @@ +{ + "name": "Prepare toolbox release", + "description": "Test toolbox code across supported MATLAB releases, package toolbox and create a draft release on GitHub. This workflow supports modular replacement for individual jobs.", + "iconName": "octicon gift", + "categories": [ + "testing", + "utilities", + "MATLAB" + ] +} diff --git a/.github/workflow-templates/prepare-release-modular.yml b/.github/workflow-templates/prepare-release-modular.yml new file mode 100644 index 0000000..71df0ac --- /dev/null +++ b/.github/workflow-templates/prepare-release-modular.yml @@ -0,0 +1,82 @@ +name: Prepare Release (Modular) + +# CONFIGURATION GUIDE: +# This workflow uses modular reusable job workflows that can be customized by changing the values below. +# To customize for your project, find and replace these values throughout the workflow: +# +# 1. CODE_DIRECTORY (default: 'code') +# - Directory containing your MATLAB source code (relative to repository root) +# - Find: code_directory: code +# - Replace with your source directory, e.g.: code_directory: src +# +# 2. TOOLS_DIRECTORY (default: 'tools') +# - Directory containing tools and MLToolboxInfo.json (relative to repository root) +# - Find: tools_directory: tools +# - Replace with your tools directory, e.g.: tools_directory: build +# +# 3. NEEDS_PYTHON (default: false) +# - Set to true if your toolbox requires Python for testing +# - This includes Python versions in test matrix and sets up Python environments +# - Find: needs_python: false +# - Replace with: needs_python: true (if Python is required) +# +# 4. MATLAB_PRODUCTS (default: '') +# - Optional MATLAB products to install during testing (space-separated list) +# - Find: matlab_products: '' +# - Replace with products, e.g.: matlab_products: 'Simulink Signal_Processing_Toolbox' + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version number in major.minor.patch format' + required: true + type: string + +jobs: + validate_version: + name: Validate version number + uses: ehennestad/matbox/.github/workflows/reusable-job_validate-version.yml@make-release-workflow-reusable + with: + version: ${{ inputs.version }} + ref_name: ${{ github.ref_name }} + tools_directory: tools + + build_matrix: + name: Build release test matrix + uses: ehennestad/matbox/.github/workflows/reusable-job_build-matrix.yml@make-release-workflow-reusable + with: + tools_directory: tools + needs_python: false + + test: + name: Run MATLAB tests + needs: [validate_version, build_matrix] + uses: ehennestad/matbox/.github/workflows/reusable-job_run-tests.yml@make-release-workflow-reusable + with: + code_directory: code + tools_directory: tools + matlab_products: '' + needs_python: false + matrix_json: ${{ needs.build_matrix.outputs.matrix }} + + release: + name: Package toolbox and create draft release + needs: [test, validate_version] + uses: ehennestad/matbox/.github/workflows/reusable-job_package-toolbox.yml@make-release-workflow-reusable + with: + version_number: ${{ needs.validate_version.outputs.version_number }} + code_directory: code + tools_directory: tools + secrets: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + + verify_installation: + name: Verify toolbox installation + needs: [release, validate_version] + uses: ehennestad/matbox/.github/workflows/reusable-job_verify-installation.yml@make-release-workflow-reusable + with: + toolbox_name: ${{ needs.release.outputs.toolbox_name }} diff --git a/.github/workflow-templates/prepare-release.properties.json b/.github/workflow-templates/prepare-release.properties.json new file mode 100644 index 0000000..8650a6c --- /dev/null +++ b/.github/workflow-templates/prepare-release.properties.json @@ -0,0 +1,10 @@ +{ + "name": "Prepare toolbox release", + "description": "Test toolbox code across supported MATLAB releases, package toolbox and create a draft release on GitHub.", + "iconName": "octicon gift", + "categories": [ + "testing", + "utilities", + "MATLAB" + ] +} diff --git a/.github/workflow-templates/prepare-release.yml b/.github/workflow-templates/prepare-release.yml new file mode 100644 index 0000000..ad59ca2 --- /dev/null +++ b/.github/workflow-templates/prepare-release.yml @@ -0,0 +1,61 @@ +name: Prepare toolbox release + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' # Matches tags like v1.2.3 + workflow_dispatch: + inputs: + version: + description: 'Version number in major.minor.patch format' + required: true + type: string + +jobs: + reuse-workflow-release: + name: Prepare toolbox release + uses: ehennestad/matbox/.github/workflows/reusable_release_workflow.yml@make-release-workflow-reusable + with: + # Do not change + version: ${{ inputs.version }} + + # Do not change + ref_name: ${{ github.ref_name }} + + # Path to directory containing source code. This directory will be packaged + # into the toolbox. Important: This should not be a namespace directory + code_directory: code + + # Path to directory containing tools and MLToolboxInfo.json. Used for finding + # unit tests, running customized MatBox tasks, and determining toolbox metadata + tools_directory: tools + + # JSON array of MATLAB versions to test. If empty (default), versions will be + # automatically determined from MLToolboxInfo.json. Example for manual override: + # matlab_versions: '["R2023a", "R2023b", "R2024a"]' + matlab_versions: '[]' + + # Optional list of MATLAB products to install. Example if adding multiple + # products (use ">" and one product per line): + # matlab_products: > + # Image_Processing_Toolbox + # Statistics_and_Machine_Learning_Toolbox + + # JSON object mapping MATLAB versions to Python versions. Use this to override + # specific Python versions for certain MATLAB releases. Only specify versions + # you want to override - others will use latest supported release from + # https://se.mathworks.com/support/requirements/python-compatibility.html + # Example: + # python_versions: '{"R2024a": "3.10", "R2024b": "3.11"}' + python_versions: '{}' + + # Whether Python is needed for testing. Set to true if your toolbox requires + # Python functionality. When false, Python setup is skipped and only MATLAB + # versions are included in the test matrix + needs_python: false + + secrets: + # SSH deploy key for pushing to protected branches. Required for creating + # releases and updating badges. Generate an SSH key pair and add the public + # key as a deploy key with write access, then add the private key as this secret + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} diff --git a/.github/workflow-templates/test-code.yml b/.github/workflow-templates/test-code.yml index 73563c6..3848d83 100644 --- a/.github/workflow-templates/test-code.yml +++ b/.github/workflow-templates/test-code.yml @@ -3,9 +3,15 @@ name: Test code on: push: branches: [ $default-branch ] - + paths-ignore: + - '*md' + - '.github/**' + pull_request: branches: [ $default-branch ] + paths-ignore: + - '*md' + - '.github/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows-reusable/reusable-workflow-check-code.yml b/.github/workflows-reusable/reusable-workflow-check-code.yml new file mode 100644 index 0000000..507dd9b --- /dev/null +++ b/.github/workflows-reusable/reusable-workflow-check-code.yml @@ -0,0 +1,52 @@ +name: Analyse code + +on: + workflow_call: + inputs: + code_directory: + description: Path to the directory containing code. Code analysis will run on the contents of this folder and its subfolders. + type: string + default: 'code' + tools_directory: + description: Path to the directory containing CI tools. Used for locating customized MatBox tasks. + type: string + default: 'tools' + matlab_release: + description: MATLAB release to use when running code analysis. + type: string + default: 'latest' + matlab_use_cache: + description: Whether to cache the MATLAB installation for faster subsequent setups. + type: boolean + default: false +jobs: + # This workflow contains a single job called "check" + check: + name: Check code + runs-on: ubuntu-latest + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Set up MATLAB (${{ inputs.matlab_release }}) + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ inputs.matlab_release }} + cache: ${{ inputs.matlab_use_cache }} + + - name: Install MatBox + uses: ehennestad/matbox/.github/actions/install-matbox@v0.9 + + - name: Check code and upload report + uses: ehennestad/matbox/.github/actions/check-code@v0.9 + with: + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + + - name: Commit SVG badges if updated + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: ehennestad/matbox/.github/actions/push-badges@v0.9 + with: + pr-ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} + pr-repo: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..f4911f3 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,198 @@ +# Reusable GitHub Actions Workflows + +This directory contains reusable GitHub Actions workflows for MATLAB toolbox development and release management. + +## Overview + +The workflows are organized into two levels: + +1. **Complete Workflows** (`reusable-workflow_*`): Full end-to-end processes +2. **Job Workflows** (`reusable-job_*`): Individual job components that can be mixed and matched + +## Complete Workflows + +### `reusable-workflow_release.yml` + +A complete release workflow that validates versions, runs tests, packages the toolbox, and verifies installation. + +**Usage:** +```yaml +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + uses: ./.github/workflows/reusable-workflow_release.yml + with: + ref_name: ${{ github.ref_name }} + needs_python: true + secrets: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} +``` + +## Job Workflows + +Individual job workflows that can be used independently or combined in custom workflows: + +### `reusable-job_validate-version.yml` + +Validates version numbers and checks for required files. + +**Inputs:** +- `version` (optional): Version number for manual triggers +- `ref_name` (optional): GitHub ref name for tag triggers +- `tools_directory` (default: 'tools'): Directory containing tools + +**Outputs:** +- `version_number`: Validated version number + +### `reusable-job_build-matrix.yml` + +Builds test matrices for MATLAB and Python versions. + +**Inputs:** +- `tools_directory` (default: 'tools'): Directory containing tools +- `matlab_versions` (default: '[]'): JSON array of MATLAB versions +- `python_versions` (default: ''): JSON object mapping MATLAB to Python versions +- `needs_python` (default: false): Whether Python is needed + +**Outputs:** +- `matrix`: Test matrix JSON +- `matlab_versions`: MATLAB versions array + +### `reusable-job_run-tests.yml` + +Runs MATLAB tests across multiple versions. + +**Inputs:** +- `code_directory` (default: 'code'): Source code directory +- `tools_directory` (default: 'tools'): Tools directory +- `matlab_products` (default: ''): MATLAB products to install +- `needs_python` (default: false): Whether Python is needed +- `matrix_json` (required): Test matrix from build-matrix job + +### `reusable-job_package-toolbox.yml` + +Packages the toolbox and creates a GitHub release. + +**Inputs:** +- `version_number` (required): Version number for the release +- `code_directory` (default: 'code'): Source code directory +- `tools_directory` (default: 'tools'): Tools directory + +**Secrets:** +- `DEPLOY_KEY` (required): SSH deploy key for pushing to protected branches + +**Outputs:** +- `toolbox_name`: Name of the packaged toolbox +- `mltbx_path`: Path to the packaged MLTBX file + +### `reusable-job_verify-installation.yml` + +Verifies toolbox installation across multiple operating systems. + +**Inputs:** +- `toolbox_name` (required): Name of the toolbox to verify +- `os_matrix` (default: '["ubuntu-latest", "windows-latest", "macos-latest"]'): OS array + +## Customization Examples + +### Adding Virtual Display Support + +When you need to add custom setup (like virtual displays) to the test job, you can create a custom workflow that uses most of the reusable jobs but implements a custom test job: + +```yaml +name: Custom Release with Virtual Display + +on: + push: + tags: + - 'v*' + +jobs: + validate_version: + uses: ./.github/workflows/reusable-job_validate-version.yml + with: + ref_name: ${{ github.ref_name }} + + build_matrix: + uses: ./.github/workflows/reusable-job_build-matrix.yml + with: + needs_python: true + + # Custom test job with virtual display + test: + name: Run MATLAB tests with virtual display + needs: [validate_version, build_matrix] + runs-on: ubuntu-latest + strategy: + matrix: ${{ fromJSON(needs.build_matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + # Custom virtual display setup + - name: Setup virtual display + run: | + sudo apt-get install -y xvfb + echo "DISPLAY=:99.0" >> $GITHUB_ENV + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + + # Continue with standard test steps... + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ matrix.MATLABVersion }} + + - name: Run tests + uses: ehennestad/matbox/.github/actions/test-code@make-release-workflow-reusable + with: + code_directory: code + tools_directory: tools + + release: + needs: [test, validate_version] + uses: ./.github/workflows/reusable-job_package-toolbox.yml + with: + version_number: ${{ needs.validate_version.outputs.version_number }} + secrets: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + + verify_installation: + needs: [release] + uses: ./.github/workflows/reusable-job_verify-installation.yml + with: + toolbox_name: ${{ needs.release.outputs.toolbox_name }} +``` + +### Adding Custom Secrets + +If you need additional secrets for specific jobs, you can pass them through: + +```yaml +jobs: + custom_test: + name: Custom test with additional secrets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Use custom secret + - name: Setup custom service + env: + API_KEY: ${{ secrets.CUSTOM_API_KEY }} + run: | + echo "Setting up custom service with API key" + + # Continue with standard steps... +``` + +## Benefits + +1. **Flexibility**: Use individual job workflows for maximum customization +2. **Reusability**: Standard workflows for common use cases +3. **Maintainability**: Changes to core functionality only need to be made in one place +4. **Gradual Migration**: Can adopt job-by-job without breaking existing workflows diff --git a/.github/workflows/deprecated/create_release.yml b/.github/workflows/deprecated/create_release.yml new file mode 100644 index 0000000..7cc5026 --- /dev/null +++ b/.github/workflows/deprecated/create_release.yml @@ -0,0 +1,26 @@ +name: Create new release + +# Run workflow when a tag is created +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' # Matches tags like v1.2.3 + workflow_dispatch: + inputs: + version: + description: 'Version number in major.minor.patch format, i.e 0.9.x' + required: true + type: string + +jobs: + release: + uses: ehennestad/matbox/.github/workflows/reusable_release_workflow.yml@make-release-workflow-reusable + with: + version: ${{ github.event.inputs.version }} + ref_name: ${{ github.ref_name }} + code_directory: 'code' + tools_directory: 'tools' + matlab_versions: '["R2023a", "R2023b", "R2024a", "R2024b"]' + python_versions: '{"R2023a": "3.10", "R2023b": "3.11", "R2024a": "3.11", "R2024b": "3.12"}' + secrets: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} diff --git a/.github/workflows/create_release.yml b/.github/workflows/deprecated/create_release_deprecated.yml similarity index 99% rename from .github/workflows/create_release.yml rename to .github/workflows/deprecated/create_release_deprecated.yml index 601733e..2d77fa0 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/deprecated/create_release_deprecated.yml @@ -5,9 +5,9 @@ name: Create new release # Run workflow when a tag is created on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' # Matches tags like v1.2.3 +# push: +# tags: +# - 'v[0-9]+.[0-9]+.[0-9]+' # Matches tags like v1.2.3 workflow_dispatch: inputs: version: diff --git a/.github/workflows/deprecated/reusable_release_workflow.yml b/.github/workflows/deprecated/reusable_release_workflow.yml new file mode 100644 index 0000000..6d6d611 --- /dev/null +++ b/.github/workflows/deprecated/reusable_release_workflow.yml @@ -0,0 +1,223 @@ +name: Reusable Release Workflow + +on: + workflow_call: + inputs: + version: + type: string + required: false + description: 'Version number in major.minor.patch format (for manual triggers)' + ref_name: + type: string + required: false + description: 'GitHub ref name (for tag triggers)' + code_directory: + type: string + default: 'code' + description: 'Directory containing source code' + tools_directory: + type: string + default: 'tools' + description: 'Directory containing tools' + matlab_products: + description: Optional list of MATLAB products to install. + type: string + default: '' + matlab_versions: + type: string + default: '[]' + description: 'JSON array of MATLAB versions to test (optional - will be determined from MLToolboxInfo.json if not provided)' + python_versions: + type: string + default: '' + description: 'JSON object mapping MATLAB versions to Python versions' + needs_python: + type: boolean + default: false + description: 'Whether Python is needed for testing (controls if Python versions are included in test matrix)' + secrets: + DEPLOY_KEY: + required: true + description: 'SSH deploy key for pushing to protected branches' + +jobs: + validate_version: + name: Validate version number + runs-on: ubuntu-latest + outputs: + version_number: ${{ steps.validate.outputs.version_number }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for MLToolboxInfo.json + run: | + if [ ! -f "${{ inputs.tools_directory }}/MLToolboxInfo.json" ]; then + echo "Error: MLToolboxInfo.json not found in ${{ inputs.tools_directory }} directory" + echo "This file is required for the release workflow to function properly" + exit 1 + fi + echo "MLToolboxInfo.json found in ${{ inputs.tools_directory }} directory" + + - name: Validate version + id: validate + uses: ehennestad/matbox/.github/actions/validate-version@make-release-workflow-reusable + with: + version: ${{ inputs.version }} + ref_name: ${{ inputs.ref_name }} + + build_matrix: + name: Build release test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build_matrix.outputs.matrix }} + matlab_versions: ${{ steps.build_matrix.outputs.matlab_versions }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build test matrix + id: build_matrix + uses: ehennestad/matbox/.github/actions/build-test-matrix@make-release-workflow-reusable + with: + tools_directory: ${{ inputs.tools_directory }} + matlab_versions: ${{ inputs.matlab_versions }} + python_versions: ${{ inputs.python_versions }} + include_python: ${{ inputs.needs_python }} + + test: + name: Run MATLAB tests (${{ matrix.MATLABVersion }}) + needs: [validate_version, build_matrix] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.build_matrix.outputs.matrix) }} + + steps: + # Checks-out the repository under $GITHUB_WORKSPACE, so the job can access it + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + if: ${{ inputs.needs_python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.pythonVersion }} + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ matrix.MATLABVersion }} + products: ${{ inputs.matlab_products }} + + + - name: Install MatBox + uses: ehennestad/matbox/.github/actions/install-matbox@make-release-workflow-reusable + + # Runs all tests in the project. Put results in a version specific subdirectory + - name: Run tests + uses: ehennestad/matbox/.github/actions/test-code@make-release-workflow-reusable + with: + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + report_subdirectory: ${{ matrix.MATLABVersion }} + create_badge: false + + # Save the contents of the report directory from each release into an artifact. + - name: Save report directory + uses: actions/upload-artifact@v4 + if: always() + with: + name: reports-${{ matrix.MATLABVersion }} + path: docs/reports + + release: + name: Package toolbox and create draft release + needs: [test, validate_version] + runs-on: ubuntu-latest + outputs: + toolbox_name: ${{ steps.package.outputs.toolbox_name }} + steps: + - name: Checkout repository using deploy key + uses: actions/checkout@v4 + with: + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Install MatBox + uses: ehennestad/matbox/.github/actions/install-matbox@make-release-workflow-reusable + + # Generate badge + - name: Generate "tested with" badge + uses: ehennestad/matbox/.github/actions/update-badges@make-release-workflow-reusable + with: + version_number: ${{ needs.validate_version.outputs.version_number }} + tools_directory: ${{ inputs.tools_directory }} + + # Package toolbox + - name: Package toolbox + id: package + uses: ehennestad/matbox/.github/actions/package-toolbox@make-release-workflow-reusable + with: + version_number: ${{ needs.validate_version.outputs.version_number }} + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + + # Save the MLTBX. + - name: Save packaged toolbox + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.package.outputs.toolbox_name }}.mltbx + path: ${{ steps.package.outputs.mltbx_path }} + retention-days: 1 + + # Create GitHub release + - name: Create GitHub release + uses: ehennestad/matbox/.github/actions/create-github-release@make-release-workflow-reusable + with: + version_number: ${{ needs.validate_version.outputs.version_number }} + mltbx_path: ${{ steps.package.outputs.mltbx_path }} + + verify_installation: + name: Verify toolbox installation (${{ matrix.os }}) + needs: [release, validate_version] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Download packaged toolbox + uses: actions/download-artifact@v4 + with: + name: ${{ needs.release.outputs.toolbox_name }}.mltbx + + - name: Install and verify toolbox + uses: matlab-actions/run-command@v2 + with: + command: | + % Get the MLTBX file name + mltbxFiles = dir('*.mltbx'); + if isempty(mltbxFiles) + error('No MLTBX file found'); + end + mltbxFile = mltbxFiles(1).name; + + % Install the toolbox + fprintf('Installing toolbox: %s\n', mltbxFile); + agreeToLicense = true; + installedToolbox = matlab.addons.install(mltbxFile, agreeToLicense); + + % Verify installation + fprintf('Toolbox installed successfully:\n'); + fprintf(' Name: %s\n', installedToolbox.Name); + fprintf(' Version: %s\n', installedToolbox.Version); + fprintf(' Identifier: %s\n', installedToolbox.Identifier); + + fprintf('Verification completed successfully on %s\n', computer); diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index e977773..8784289 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -3,7 +3,7 @@ name: Prepare toolbox release on: push: tags: - - 'v*' + - 'v[0-9]+.[0-9]+.[0-9]+' # Matches tags like v1.2.3 workflow_dispatch: inputs: version: @@ -12,8 +12,8 @@ on: type: string jobs: - reuse-workflow-release: - name: Prepare toolbox release + release: + name: Prepare toolbox release (MatBox) uses: ehennestad/matbox/.github/workflows/reusable_release_workflow.yml@make-release-workflow-reusable with: # Do not change diff --git a/.github/workflows/reusable-job_build-matrix.yml b/.github/workflows/reusable-job_build-matrix.yml new file mode 100644 index 0000000..78c25b8 --- /dev/null +++ b/.github/workflows/reusable-job_build-matrix.yml @@ -0,0 +1,57 @@ +name: Build test matrix + +# Build matrix variables for running tests across different MATLAB releases. +# Will return a json with the following variables: +# - MATLABVersion +# - pythonVersion (if needs_python == true) +# The pythonVersion will be the latest compatible python version for each +# MATLAB release. +# See also: +# .github/actions/build-test-matrix/action.yml + +on: + workflow_call: + inputs: + tools_directory: + type: string + default: 'tools' + description: 'Directory containing tools' + matlab_versions: + type: string + default: '[]' + description: 'JSON array of MATLAB versions to test (optional - will be determined from MLToolboxInfo.json if not provided)' + python_versions: + type: string + default: '' + description: 'JSON object mapping MATLAB versions to Python versions' + needs_python: + type: boolean + default: false + description: 'Whether Python is needed for testing (controls if Python versions are included in test matrix)' + outputs: + matrix: + description: 'Test matrix JSON' + value: ${{ jobs.build_matrix.outputs.matrix }} + matlab_versions: + description: 'MATLAB versions array' + value: ${{ jobs.build_matrix.outputs.matlab_versions }} + +jobs: + build_matrix: + name: Build release test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build_matrix.outputs.matrix }} + matlab_versions: ${{ steps.build_matrix.outputs.matlab_versions }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build test matrix + id: build_matrix + uses: ehennestad/matbox/.github/actions/build-test-matrix@make-release-workflow-reusable + with: + tools_directory: ${{ inputs.tools_directory }} + matlab_versions: ${{ inputs.matlab_versions }} + python_versions: ${{ inputs.python_versions }} + include_python: ${{ inputs.needs_python }} diff --git a/.github/workflows/reusable-job_package-toolbox.yml b/.github/workflows/reusable-job_package-toolbox.yml new file mode 100644 index 0000000..32fe2c2 --- /dev/null +++ b/.github/workflows/reusable-job_package-toolbox.yml @@ -0,0 +1,78 @@ +name: Package toolbox + +on: + workflow_call: + inputs: + version_number: + type: string + required: true + description: 'Version number for the release' + code_directory: + type: string + default: 'code' + description: 'Directory containing source code' + tools_directory: + type: string + default: 'tools' + description: 'Directory containing tools' + secrets: + DEPLOY_KEY: + required: true + description: 'SSH deploy key for pushing to protected branches' + outputs: + toolbox_name: + description: 'Name of the packaged toolbox' + value: ${{ jobs.release.outputs.toolbox_name }} + mltbx_path: + description: 'Path to the packaged MLTBX file' + value: ${{ jobs.release.outputs.mltbx_path }} + +jobs: + release: + name: Package toolbox and create draft release + runs-on: ubuntu-latest + outputs: + toolbox_name: ${{ steps.package.outputs.toolbox_name }} + mltbx_path: ${{ steps.package.outputs.mltbx_path }} + steps: + - name: Checkout repository using deploy key + uses: actions/checkout@v4 + with: + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Install MatBox + uses: ehennestad/matbox/.github/actions/install-matbox@make-release-workflow-reusable + + # Generate badge + - name: Generate "tested with" badge + uses: ehennestad/matbox/.github/actions/update-badges@make-release-workflow-reusable + with: + version_number: ${{ inputs.version_number }} + tools_directory: ${{ inputs.tools_directory }} + + # Package toolbox + - name: Package toolbox + id: package + uses: ehennestad/matbox/.github/actions/package-toolbox@make-release-workflow-reusable + with: + version_number: ${{ inputs.version_number }} + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + + # Save the MLTBX. + - name: Save packaged toolbox + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.package.outputs.toolbox_name }}.mltbx + path: ${{ steps.package.outputs.mltbx_path }} + retention-days: 1 + + # Create GitHub release + - name: Create GitHub release + uses: ehennestad/matbox/.github/actions/create-github-release@make-release-workflow-reusable + with: + version_number: ${{ inputs.version_number }} + mltbx_path: ${{ steps.package.outputs.mltbx_path }} diff --git a/.github/workflows/reusable-job_run-tests.yml b/.github/workflows/reusable-job_run-tests.yml new file mode 100644 index 0000000..6225100 --- /dev/null +++ b/.github/workflows/reusable-job_run-tests.yml @@ -0,0 +1,70 @@ +name: Run Tests + +on: + workflow_call: + inputs: + code_directory: + type: string + default: 'code' + description: 'Directory containing source code' + tools_directory: + type: string + default: 'tools' + description: 'Directory containing tools' + matlab_products: + description: Optional list of MATLAB products to install. + type: string + default: '' + needs_python: + type: boolean + default: false + description: 'Whether Python is needed for testing (controls if Python versions are included in test matrix)' + matrix_json: + type: string + required: true + description: 'JSON string containing the test matrix from build-matrix job' + +jobs: + test: + name: Run MATLAB tests (${{ matrix.MATLABVersion }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(inputs.matrix_json) }} + + steps: + # Checks-out the repository under $GITHUB_WORKSPACE, so the job can access it + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + if: ${{ inputs.needs_python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.pythonVersion }} + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ matrix.MATLABVersion }} + products: ${{ inputs.matlab_products }} + + - name: Install MatBox + uses: ehennestad/matbox/.github/actions/install-matbox@make-release-workflow-reusable + + # Run all tests in the project. Put results in a version specific subdirectory + - name: Run tests + uses: ehennestad/matbox/.github/actions/test-code@make-release-workflow-reusable + with: + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + report_subdirectory: ${{ matrix.MATLABVersion }} + create_badge: false + + # Save the contents of the report directory from each release into an artifact. + - name: Save report directory + uses: actions/upload-artifact@v4 + if: always() + with: + name: reports-${{ matrix.MATLABVersion }} + path: docs/reports diff --git a/.github/workflows/reusable-job_validate-version.yml b/.github/workflows/reusable-job_validate-version.yml new file mode 100644 index 0000000..c1a1f40 --- /dev/null +++ b/.github/workflows/reusable-job_validate-version.yml @@ -0,0 +1,47 @@ +name: Validate version number + +on: + workflow_call: + inputs: + version: + type: string + required: false + description: 'Version number in major.minor.patch format (for manual triggers)' + ref_name: + type: string + required: false + description: 'GitHub ref name (for tag triggers)' + tools_directory: + type: string + default: 'tools' + description: 'Directory containing tools' + outputs: + version_number: + description: 'Validated version number' + value: ${{ jobs.validate_version.outputs.version_number }} + +jobs: + validate_version: + name: Validate version number + runs-on: ubuntu-latest + outputs: + version_number: ${{ steps.validate.outputs.version_number }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for MLToolboxInfo.json + run: | + if [ ! -f "${{ inputs.tools_directory }}/MLToolboxInfo.json" ]; then + echo "Error: MLToolboxInfo.json not found in ${{ inputs.tools_directory }} directory" + echo "This file is required for the release workflow to function properly" + exit 1 + fi + echo "MLToolboxInfo.json found in ${{ inputs.tools_directory }} directory" + + - name: Validate version + id: validate + uses: ehennestad/matbox/.github/actions/validate-version@make-release-workflow-reusable + with: + version: ${{ inputs.version }} + ref_name: ${{ inputs.ref_name }} diff --git a/.github/workflows/reusable-job_verify-installation.yml b/.github/workflows/reusable-job_verify-installation.yml new file mode 100644 index 0000000..942f26f --- /dev/null +++ b/.github/workflows/reusable-job_verify-installation.yml @@ -0,0 +1,55 @@ +name: Verify toolbox installation + +on: + workflow_call: + inputs: + toolbox_name: + type: string + required: true + description: 'Name of the toolbox to verify' + os_matrix: + type: string + default: '["ubuntu-latest", "windows-latest", "macos-latest"]' + description: 'JSON array of operating systems to test on' + +jobs: + verify_installation: + name: Verify toolbox installation (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(inputs.os_matrix) }} + + steps: + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Download packaged toolbox + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.toolbox_name }}.mltbx + + - name: Install and verify toolbox + uses: matlab-actions/run-command@v2 + with: + command: | + % Get the MLTBX file name + mltbxFiles = dir('*.mltbx'); + if isempty(mltbxFiles) + error('No MLTBX file found'); + end + mltbxFile = mltbxFiles(1).name; + + % Install the toolbox + fprintf('Installing toolbox: %s\n', mltbxFile); + agreeToLicense = true; + installedToolbox = matlab.addons.install(mltbxFile, agreeToLicense); + + % Verify installation + fprintf('Toolbox installed successfully:\n'); + fprintf(' Name: %s\n', installedToolbox.Name); + fprintf(' Version: %s\n', installedToolbox.Version); + fprintf(' Identifier: %s\n', installedToolbox.Identifier); + + fprintf('Verification completed successfully on %s\n', computer); diff --git a/.github/workflows/reusable-workflow_release.yml b/.github/workflows/reusable-workflow_release.yml new file mode 100644 index 0000000..122f962 --- /dev/null +++ b/.github/workflows/reusable-workflow_release.yml @@ -0,0 +1,88 @@ +name: Reusable Workflow - Release + +on: + workflow_call: + inputs: + version: + type: string + required: false + description: 'Version number in major.minor.patch format (for manual triggers)' + ref_name: + type: string + required: false + description: 'GitHub ref name (for tag triggers)' + code_directory: + type: string + default: 'code' + description: 'Directory containing source code' + tools_directory: + type: string + default: 'tools' + description: 'Directory containing tools' + matlab_products: + description: Optional list of MATLAB products to install. + type: string + default: '' + matlab_versions: + type: string + default: '[]' + description: 'JSON array of MATLAB versions to test (optional - will be determined from MLToolboxInfo.json if not provided)' + python_versions: + type: string + default: '' + description: 'JSON object mapping MATLAB versions to Python versions' + needs_python: + type: boolean + default: false + description: 'Whether Python is needed for testing (controls if Python versions are included in test matrix)' + secrets: + DEPLOY_KEY: + required: true + description: 'SSH deploy key for pushing to protected branches' + +jobs: + validate_version: + name: Validate version number + uses: ehennestad/matbox/.github/workflows/reusable-job_validate-version.yml@make-release-workflow-reusable + with: + version: ${{ inputs.version }} + ref_name: ${{ inputs.ref_name }} + tools_directory: ${{ inputs.tools_directory }} + + build_matrix: + name: Build release test matrix + uses: ehennestad/matbox/.github/workflows/reusable-job_build-matrix.yml@make-release-workflow-reusable + with: + tools_directory: ${{ inputs.tools_directory }} + matlab_versions: ${{ inputs.matlab_versions }} + python_versions: ${{ inputs.python_versions }} + needs_python: ${{ inputs.needs_python }} + + test: + name: Run MATLAB tests + needs: [validate_version, build_matrix] + uses: ehennestad/matbox/.github/workflows/reusable-job_run-tests.yml@make-release-workflow-reusable + with: + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + matlab_products: ${{ inputs.matlab_products }} + needs_python: ${{ inputs.needs_python }} + matrix_json: ${{ needs.build_matrix.outputs.matrix }} + + release: + name: Package toolbox and create draft release + needs: [test, validate_version] + uses: ehennestad/matbox/.github/workflows/reusable-job_package-toolbox.yml@make-release-workflow-reusable + with: + version_number: ${{ needs.validate_version.outputs.version_number }} + code_directory: ${{ inputs.code_directory }} + tools_directory: ${{ inputs.tools_directory }} + secrets: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + + verify_installation: + name: Verify toolbox installation + needs: [release, validate_version] + uses: ehennestad/matbox/.github/workflows/reusable-job_verify-installation.yml@make-release-workflow-reusable + with: + toolbox_name: ${{ needs.release.outputs.toolbox_name }} diff --git a/.github/workflows/reusable_check_code.yml b/.github/workflows/reusable_check_code.yml index fbc4c19..507dd9b 100644 --- a/.github/workflows/reusable_check_code.yml +++ b/.github/workflows/reusable_check_code.yml @@ -15,7 +15,10 @@ on: description: MATLAB release to use when running code analysis. type: string default: 'latest' - + matlab_use_cache: + description: Whether to cache the MATLAB installation for faster subsequent setups. + type: boolean + default: false jobs: # This workflow contains a single job called "check" check: @@ -30,7 +33,7 @@ jobs: uses: matlab-actions/setup-matlab@v2 with: release: ${{ inputs.matlab_release }} - cache: true + cache: ${{ inputs.matlab_use_cache }} - name: Install MatBox uses: ehennestad/matbox/.github/actions/install-matbox@v0.9 diff --git a/.github/workflows/reusable_run_codespell.yml b/.github/workflows/reusable_run_codespell.yml index 66679f0..0544df4 100644 --- a/.github/workflows/reusable_run_codespell.yml +++ b/.github/workflows/reusable_run_codespell.yml @@ -1,4 +1,6 @@ -# NB: Will only process "skip" and "ignore-words-list" from the codespell +# Note: This workflow allows specifying a custom location for the Codespell +# configuration file by defining CONFIG_FILE as an input variable. +# Warning: Will only process "skip" and "ignore-words-list" from the codespell # config file if provided name: Run codespell diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run-tests.yml similarity index 100% rename from .github/workflows/run_tests.yml rename to .github/workflows/run-tests.yml