diff --git a/.gitattributes b/.gitattributes index 58cf167..a7550d7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -/code/MOSuite linguist-vendored \ No newline at end of file +/code/MOSuite linguist-vendored +code/MOSuite linguist-vendored diff --git a/.github/WORDLIST.txt b/.github/WORDLIST.txt new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml index 5c98340..556cc9e 100644 --- a/.github/workflows/auto-format.yml +++ b/.github/workflows/auto-format.yml @@ -3,44 +3,45 @@ name: auto-format on: workflow_dispatch: pull_request: + branches: [dev, main] -env: - GH_TOKEN: ${{ github.token }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: write pull-requests: write -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: auto-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - if: github.event_name == 'pull_request' + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@v2 with: - fetch-depth: 0 - ref: ${{ github.head_ref }} - - uses: actions/checkout@v4 - if: github.event_name != 'pull_request' + app-id: ${{ vars.CCBR_BOT_APP_ID }} + private-key: ${{ secrets.CCBR_BOT_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - uses: actions/checkout@v7 with: - fetch-depth: 0 - ref: ${{ github.ref_name }} + token: ${{ steps.generate-token.outputs.token }} + ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} - name: git config run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Format R code with air - uses: rstudio/shiny-workflows/format-r-code@v1 + git config --global user.name "CCBR-bot" + git config --global user.email "258092125+ccbr-bot@users.noreply.github.com" + - name: format + uses: pre-commit/action@v3.0.1 + continue-on-error: true - name: commit & push + if: ${{ github.event_name == 'pull_request' }} run: | git add . - git commit -m "ci: 🤖 auto-format" || echo "nothing to commit" + git commit -m "ci: 🤖 auto-format with pre-commit" || echo "nothing to commit" git push || echo "nothing to push" diff --git a/.github/workflows/syncweaver-host-contribute-patch.yml b/.github/workflows/syncweaver-host-contribute-patch.yml new file mode 100644 index 0000000..e6a2d3d --- /dev/null +++ b/.github/workflows/syncweaver-host-contribute-patch.yml @@ -0,0 +1,190 @@ +name: syncweaver-contribute-patch + +on: + workflow_dispatch: + inputs: + source_path: + description: Optional tracked source path override; if omitted, source_path is resolved from lockfile. + required: false + type: string + repo_url: + description: Optional source repository identifier (URL or OWNER/REPO) used to resolve source_path from lockfile. + required: false + type: string + source_repository: + description: Optional source repository in OWNER/REPO format; if omitted, derives from lockfile repo_url. + required: false + type: string + lockfile: + description: Path to the syncweaver lockfile. + required: false + default: .syncweaver-lock.json + type: string + patch_path: + description: Optional patch path override; if omitted, uses source entry patch value from lockfile. + required: false + type: string + source_base_ref: + description: Optional source repository base branch for PR; if omitted, uses source entry ref from lockfile. + required: false + type: string + +permissions: + contents: read + +jobs: + resolve-patch-metadata: + runs-on: ubuntu-latest + outputs: + source_path: ${{ steps.resolve.outputs.source_path }} + repo_url: ${{ steps.resolve.outputs.repo_url }} + source_repository: ${{ steps.resolve.outputs.source_repository }} + patch_path: ${{ steps.resolve.outputs.patch_path }} + source_base_ref: ${{ steps.resolve.outputs.source_base_ref }} + lockfile: ${{ env.LOCKFILE }} + env: + SOURCE_PATH: ${{ github.event.inputs.source_path || '' }} + REPO_URL: ${{ github.event.inputs.repo_url || '' }} + SOURCE_REPOSITORY: ${{ github.event.inputs.source_repository || '' }} + LOCKFILE: ${{ github.event.inputs.lockfile || '.syncweaver-lock.json' }} + PATCH_PATH: ${{ github.event.inputs.patch_path || '' }} + SOURCE_BASE_REF: ${{ github.event.inputs.source_base_ref || '' }} + steps: + - name: Checkout host repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: pip + + - name: Install syncweaver + run: | + python -m pip install --upgrade pip + python -m pip install "git+https://github.com/CCBR/syncweaver.git@main" + + - name: Resolve source and patch metadata + id: resolve + shell: bash + env: + SOURCE_PATH_INPUT: ${{ env.SOURCE_PATH }} + REPO_URL_INPUT: ${{ env.REPO_URL }} + SOURCE_REPOSITORY_INPUT: ${{ env.SOURCE_REPOSITORY }} + LOCKFILE_PATH: ${{ env.LOCKFILE }} + PATCH_PATH_INPUT: ${{ env.PATCH_PATH }} + SOURCE_BASE_REF_INPUT: ${{ env.SOURCE_BASE_REF }} + SYNCWEAVER_HOST_CWD: ${{ github.workspace }} + run: | + python -m syncweaver.contribute_patch + + - name: Print resolved metadata + shell: bash + run: | + set -euo pipefail + echo "source_path=${{ steps.resolve.outputs.source_path }}" + echo "repo_url=${{ steps.resolve.outputs.repo_url }}" + echo "source_repository=${{ steps.resolve.outputs.source_repository }}" + echo "patch_path=${{ steps.resolve.outputs.patch_path }}" + echo "source_base_ref=${{ steps.resolve.outputs.source_base_ref }}" + + contribute-patch: + runs-on: ubuntu-latest + needs: resolve-patch-metadata + permissions: + contents: read + env: + SOURCE_PATH: ${{ needs.resolve-patch-metadata.outputs.source_path }} + REPO_URL: ${{ needs.resolve-patch-metadata.outputs.repo_url }} + SOURCE_REPOSITORY: ${{ needs.resolve-patch-metadata.outputs.source_repository }} + PATCH_PATH: ${{ needs.resolve-patch-metadata.outputs.patch_path }} + SOURCE_BASE_REF: ${{ needs.resolve-patch-metadata.outputs.source_base_ref }} + LOCKFILE: ${{ needs.resolve-patch-metadata.outputs.lockfile }} + steps: + - name: Generate CCBR-bot token + id: ccbr_bot + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CCBR_BOT_APP_ID }} + private-key: ${{ secrets.CCBR_BOT_PRIVATE_KEY }} + + - name: Checkout host repository + uses: actions/checkout@v6 + with: + path: host + + - name: Checkout source repository + uses: actions/checkout@v6 + with: + repository: ${{ env.SOURCE_REPOSITORY }} + ref: ${{ env.SOURCE_BASE_REF }} + token: ${{ steps.ccbr_bot.outputs.token }} + path: source + + - name: Apply patch to source repository + id: apply_patch + shell: bash + run: | + set -euo pipefail + + patch_file="$GITHUB_WORKSPACE/host/${PATCH_PATH}" + if [[ ! -f "$patch_file" ]]; then + echo "Error: patch file not found in host repository: ${PATCH_PATH}" >&2 + exit 1 + fi + + branch_stub=$(printf '%s' "$SOURCE_PATH" | tr '/ ' '--' | tr -cd '[:alnum:]._-') + if [[ -z "$branch_stub" ]]; then + branch_stub="patch" + fi + branch_name="syncweaver/contribute-patch/${branch_stub}-${GITHUB_RUN_ID}" + + cd "$GITHUB_WORKSPACE/source" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$branch_name" + + if ! git apply --3way --whitespace=nowarn "$patch_file"; then + echo "Error: patch failed to apply cleanly to ${SOURCE_REPOSITORY}@${SOURCE_BASE_REF}" >&2 + exit 1 + fi + + if git diff --quiet; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT" + + - name: Open pull request in source repository + if: steps.apply_patch.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.ccbr_bot.outputs.token }} + path: source + base: ${{ env.SOURCE_BASE_REF }} + branch: ${{ steps.apply_patch.outputs.branch_name }} + delete-branch: true + commit-message: "chore(syncweaver): apply host patch for ${{ env.SOURCE_PATH }}" + title: "chore(syncweaver): apply host patch for ${{ env.SOURCE_PATH }}" + body: | + Automated patch contribution from host repository workflow dispatch. + + Inputs used: + - source_path: ${{ env.SOURCE_PATH }} + - repo_url: ${{ env.REPO_URL }} + - source_repository: ${{ env.SOURCE_REPOSITORY }} + - source_base_ref: ${{ env.SOURCE_BASE_REF }} + - patch_path: ${{ env.PATCH_PATH }} + - lockfile: ${{ env.LOCKFILE }} + + Trigger context: + - host_repository: ${{ github.repository }} + - workflow_run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - name: Print no-op summary + if: steps.apply_patch.outputs.has_changes == 'false' + shell: bash + run: | + set -euo pipefail + echo "Patch applied cleanly but introduced no source changes. No PR created." diff --git a/.github/workflows/syncweaver-host-update.yml b/.github/workflows/syncweaver-host-update.yml new file mode 100644 index 0000000..4e372a3 --- /dev/null +++ b/.github/workflows/syncweaver-host-update.yml @@ -0,0 +1,172 @@ +name: syncweaver-host-update + +on: + repository_dispatch: + workflow_dispatch: + inputs: + source_path: + description: Optional tracked source path override for manual runs; if omitted, source_path is resolved from the lockfile. + required: false + type: string + repo_url: + description: Optional source repository identifier (URL or OWNER/REPO) used to resolve source_path from lockfile; for repository_dispatch this defaults to client_payload.source_repository. + required: false + type: string + lockfile: + description: Path to the syncweaver lockfile. + required: false + default: .syncweaver-lock.json + type: string + ref: + description: Optional git ref to sync from. + required: false + type: string + remote_subdir: + description: Optional remote subdirectory to vendor. + required: false + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + resolve-source-paths: + runs-on: ubuntu-latest + outputs: + source_paths: ${{ steps.resolve_source_paths.outputs.source_paths }} + source_count: ${{ steps.resolve_source_paths.outputs.source_count }} + lockfile: ${{ env.LOCKFILE }} + ref: ${{ env.REF }} + remote_subdir: ${{ env.REMOTE_SUBDIR }} + repo_url: ${{ env.REPO_URL }} + env: + # workflow_dispatch may provide source_path directly; repository_dispatch usually + # omits it so the workflow resolves source_path from lockfile + source_repository. + SOURCE_PATH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.source_path || github.event.client_payload.source_path || '' }} + # Prefer explicit repo_url, then fall back to source_repository from dispatch payload. + REPO_URL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.repo_url || github.event.client_payload.repo_url || github.event.client_payload.source_repository || '' }} + LOCKFILE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.lockfile || github.event.client_payload.lockfile || '.syncweaver-lock.json' }} + REF: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.event.client_payload.ref || '' }} + REMOTE_SUBDIR: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.remote_subdir || github.event.client_payload.remote_subdir || '' }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: pip + + - name: Install syncweaver + run: | + python -m pip install --upgrade pip + python -m pip install "git+https://github.com/CCBR/syncweaver.git@main" + + - name: Resolve source paths + id: resolve_source_paths + shell: python + env: + SOURCE_PATH_INPUT: ${{ env.SOURCE_PATH }} + REPO_URL_INPUT: ${{ env.REPO_URL }} + LOCKFILE_PATH: ${{ env.LOCKFILE }} + run: | + import json + import os + import pathlib + + from syncweaver.lockfile import resolve_source_paths_from_lockfile + + source_path_input = os.environ["SOURCE_PATH_INPUT"].strip() + repo_url_input = os.environ["REPO_URL_INPUT"].strip() + lockfile_path = pathlib.Path(os.environ["LOCKFILE_PATH"]) + output_path = pathlib.Path(os.environ["GITHUB_OUTPUT"]) + + try: + resolved_source_paths = resolve_source_paths_from_lockfile( + lockfile=lockfile_path, + source_path=source_path_input, + repo_url=repo_url_input, + ) + except ValueError as exc: + raise SystemExit(f"Error: {exc}") from exc + + with output_path.open("a", encoding="utf-8") as fh: + fh.write(f"source_paths={json.dumps(resolved_source_paths)}\n") + fh.write(f"source_count={len(resolved_source_paths)}\n") + + - name: Print resolved source paths + shell: bash + run: | + set -euo pipefail + echo "source_count=${{ steps.resolve_source_paths.outputs.source_count }}" + echo "source_paths=${{ steps.resolve_source_paths.outputs.source_paths }}" + + update-source: + runs-on: ubuntu-latest + needs: resolve-source-paths + strategy: + fail-fast: false + matrix: + source_path: ${{ fromJson(needs.resolve-source-paths.outputs.source_paths) }} + env: + LOCKFILE: ${{ needs.resolve-source-paths.outputs.lockfile }} + REF: ${{ needs.resolve-source-paths.outputs.ref }} + REMOTE_SUBDIR: ${{ needs.resolve-source-paths.outputs.remote_subdir }} + REPO_URL: ${{ needs.resolve-source-paths.outputs.repo_url }} + steps: + - name: Generate CCBR-bot token + id: ccbr_bot + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.CCBR_BOT_APP_ID }} + private-key: ${{ secrets.CCBR_BOT_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ steps.ccbr_bot.outputs.token }} + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: pip + + - name: Install syncweaver + run: | + python -m pip install --upgrade pip + python -m pip install "git+https://github.com/CCBR/syncweaver.git@main" + + - name: Run update-source composite action + id: update_source + uses: CCBR/syncweaver/actions/update-source@latest + with: + source_path: ${{ matrix.source_path }} + lockfile: ${{ env.LOCKFILE }} + ref: ${{ env.REF }} + remote_subdir: ${{ env.REMOTE_SUBDIR }} + + - name: Commit changes and open PR + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.ccbr_bot.outputs.token }} + branch: syncweaver/update-source/${{ matrix.source_path }} + delete-branch: true + commit-message: "chore(syncweaver): update ${{ matrix.source_path }} from ${{ steps.update_source.outputs.source_repo }}@${{ steps.update_source.outputs.ref }}" + title: "chore(syncweaver): update ${{ matrix.source_path }} from ${{ steps.update_source.outputs.source_repo }}@${{ steps.update_source.outputs.ref }}" + body: | + Automated source update from the update-source workflow. + + Inputs used: + - source_path: ${{ matrix.source_path }} + - repo_url: ${{ env.REPO_URL }} + - lockfile: ${{ env.LOCKFILE }} + - ref: ${{ env.REF }} + - remote_subdir: ${{ env.REMOTE_SUBDIR }} + + Resolved from lockfile after update: + - source_url: ${{ steps.update_source.outputs.source_url }} + - ref: ${{ steps.update_source.outputs.ref }} + - remote_subdir: ${{ steps.update_source.outputs.remote_subdir }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 61d89f6..d3bacfe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,15 +2,19 @@ name: tests on: push: - branches: [ main, dev ] + branches: [main, dev] pull_request: - branches: [ main, dev ] + branches: [main, dev] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Parse environment.json and set Docker image id: docker_image diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..75390d2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,52 @@ +default_install_hook_types: [pre-commit, commit-msg] +default_stages: [pre-commit] +exclude: | + (?x)( + ^.codeocean/| + ^environment/| + ^metadata/| + ^code/MOSuite| + ^code/SCWorkflow| + ^code/OMIX* + ) + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + args: ["--maxkb=10240"] + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-json + # spell check + - repo: https://github.com/codespell-project/codespell + rev: v2.4.2 + hooks: + - id: codespell + args: + [ -I, .github/WORDLIST.txt ] + # R formatting + - repo: https://github.com/posit-dev/air-pre-commit + rev: 0.10.0 + hooks: + - id: air-format + - repo: https://github.com/lorenzwalthert/precommit + rev: v0.4.3.9028 + hooks: + - id: parsable-R + # Python formatting + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.16 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + # enforce conventional commit format + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.4.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [] diff --git a/.syncweaver-lock.json b/.syncweaver-lock.json new file mode 100644 index 0000000..e770d45 --- /dev/null +++ b/.syncweaver-lock.json @@ -0,0 +1,12 @@ +{ + "host": "NIDAP-Community/MOSuite-diff-counts", + "orchestrator": "NIDAP-Community/syncweaver-orchestrator", + "syncweaver_version": "0.0.1-dev", + "sources": { + "code/MOSuite": { + "repo_url": "https://github.com/CCBR/MOSuite", + "ref": "v0.3.2", + "git_sha": "f4465c58a7d04f5feb41d4455a019ca4946a4ca8" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d9176..fd9185b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Development version - Improved the Code Ocean parameter UI for the diff counts capsule (#3, @phoman14). +- Use MOSuite v0.3.2. (#4, @kelly-sovacool) ## v3.0 diff --git a/code/MOSuite/.github/package-versions.txt b/code/MOSuite/.github/package-versions.txt deleted file mode 100644 index 8d99fc0..0000000 --- a/code/MOSuite/.github/package-versions.txt +++ /dev/null @@ -1 +0,0 @@ -any::ggplot2@3.5.2 diff --git a/code/MOSuite/.github/requirements.txt b/code/MOSuite/.github/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/code/MOSuite/.github/workflows/R-CMD-check.yaml b/code/MOSuite/.github/workflows/R-CMD-check.yaml index 97d3f32..e4a69e6 100644 --- a/code/MOSuite/.github/workflows/R-CMD-check.yaml +++ b/code/MOSuite/.github/workflows/R-CMD-check.yaml @@ -32,13 +32,15 @@ jobs: R_KEEP_PKG_SOURCE: yes steps: - uses: actions/checkout@v6 - - uses: CCBR/actions/install-r-pak@main + - uses: r-lib/actions/setup-r@v2 with: - versions-file: .github/package-versions.txt - extra-packages: local::. - needs: dev r-version: ${{ matrix.config.r }} http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: local::. + needs: dev - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true @@ -47,10 +49,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: CCBR/actions/install-r-pak@main + - uses: r-lib/actions/setup-r@v2 + with: + r-version: '4.5' + use-public-rspm: true + - uses: r-lib/actions/setup-r-dependencies@v2 with: - r-version: 4.5 - versions-file: .github/package-versions.txt needs: dev - name: Lint shell: Rscript {0} diff --git a/code/MOSuite/.github/workflows/auto-format.yml b/code/MOSuite/.github/workflows/auto-format.yml index bc5d53b..758c5ba 100644 --- a/code/MOSuite/.github/workflows/auto-format.yml +++ b/code/MOSuite/.github/workflows/auto-format.yml @@ -38,10 +38,13 @@ jobs: git config --global user.name "CCBR-bot" git config --global user.email "258092125+ccbr-bot@users.noreply.github.com" - - uses: CCBR/actions/install-r-pak@main + - uses: r-lib/actions/setup-r@v2 + with: + r-version: '4.5' + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 with: - r-version: 4.5 - versions-file: .github/package-versions.txt extra-packages: local::. needs: dev diff --git a/code/MOSuite/.github/workflows/draft-release.yml b/code/MOSuite/.github/workflows/draft-release.yml new file mode 100644 index 0000000..3b847ca --- /dev/null +++ b/code/MOSuite/.github/workflows/draft-release.yml @@ -0,0 +1,31 @@ +name: draft-release + +on: + workflow_dispatch: + inputs: + version-tag: + description: | + Semantic version tag for next release. + If not provided, it will be determined based on conventional commit history. + Example: v2.5.11 + required: false + type: string + default: "" + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + draft-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # required to include tags + - uses: CCBR/actions/draft-release@latest + with: + github-token: ${{ github.token }} + version-tag: ${{ github.event.inputs.version-tag }} + ccbr-actions-version: latest diff --git a/code/MOSuite/.github/workflows/pkgdown.yaml b/code/MOSuite/.github/workflows/pkgdown.yaml index ac57479..12ff76a 100644 --- a/code/MOSuite/.github/workflows/pkgdown.yaml +++ b/code/MOSuite/.github/workflows/pkgdown.yaml @@ -27,10 +27,13 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: CCBR/actions/install-r-pak@main + - uses: r-lib/actions/setup-r@v2 + with: + r-version: '4.5' + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 with: - r-version: 4.5 - versions-file: .github/package-versions.txt extra-packages: local::. needs: dev diff --git a/code/MOSuite/.github/workflows/post-release.yml b/code/MOSuite/.github/workflows/post-release.yml new file mode 100644 index 0000000..81ac73f --- /dev/null +++ b/code/MOSuite/.github/workflows/post-release.yml @@ -0,0 +1,24 @@ +name: post-release + +on: + release: + types: + - published + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: CCBR/actions/post-release@latest + with: + github-token: ${{ github.token }} + update-sliding-tags: false + ccbr-actions-version: latest diff --git a/code/MOSuite/.github/workflows/test-coverage.yaml b/code/MOSuite/.github/workflows/test-coverage.yaml index 6531d6f..26f3f0a 100644 --- a/code/MOSuite/.github/workflows/test-coverage.yaml +++ b/code/MOSuite/.github/workflows/test-coverage.yaml @@ -35,10 +35,13 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: CCBR/actions/install-r-pak@main + - uses: r-lib/actions/setup-r@v2 + with: + r-version: '4.5' + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 with: - r-version: 4.5 - versions-file: .github/package-versions.txt needs: dev - name: Test coverage diff --git a/code/MOSuite/.pre-commit-config.yaml b/code/MOSuite/.pre-commit-config.yaml index 7331cb1..a09d9f3 100644 --- a/code/MOSuite/.pre-commit-config.yaml +++ b/code/MOSuite/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: hooks: - id: air-format - repo: https://github.com/lorenzwalthert/precommit - rev: v0.4.3.9021 + rev: v0.4.3.9025 hooks: - id: parsable-R - id: readme-rmd-rendered diff --git a/code/MOSuite/CITATION.cff b/code/MOSuite/CITATION.cff index 541c136..9768535 100644 --- a/code/MOSuite/CITATION.cff +++ b/code/MOSuite/CITATION.cff @@ -1,14 +1,14 @@ -# -------------------------------------------- -# CITATION file created with {cffr} R package +# ------------------------------------------------ +# CITATION.cff file created with {cffr} R package # See also: https://docs.ropensci.org/cffr/ -# -------------------------------------------- +# ------------------------------------------------ cff-version: 1.2.0 message: 'To cite package "MOSuite" in publications use:' type: software license: MIT title: 'MOSuite: R package for differential multi-omics analysis' -version: 0.3.1 +version: 0.3.2 doi: 10.5281/zenodo.16371580 identifiers: - description: Archived snapshots of all versions @@ -71,10 +71,13 @@ references: url: https://www.R-project.org/ authors: - name: R Core Team + website: https://ror.org/02zz1nj61 institution: name: R Foundation for Statistical Computing + website: https://ror.org/05qewa988 address: Vienna, Austria year: '2026' + doi: 10.32614/R.manuals version: '>= 4.0.0' - type: software title: assertthat @@ -215,7 +218,6 @@ references: orcid: https://orcid.org/0000-0002-9335-7468 year: '2026' doi: 10.32614/CRAN.package.ggplot2 - version: < 4.0.0 - type: software title: glue abstract: 'glue: Interpreted String Literals' @@ -278,51 +280,8 @@ references: authors: - family-names: Smyth given-names: Gordon - - family-names: Hu - given-names: Yifang - - family-names: Ritchie - given-names: Matthew - - family-names: Silver - given-names: Jeremy - - family-names: Wettenhall - given-names: James - - family-names: McCarthy - given-names: Davis - - family-names: Wu - given-names: Di - - family-names: Shi - given-names: Wei - - family-names: Phipson - given-names: Belinda - - family-names: Lun - given-names: Aaron - - family-names: Thorne - given-names: Natalie - - family-names: Oshlack - given-names: Alicia - - family-names: Graaf - given-names: Carolyn - name-particle: de - - family-names: Chen - given-names: Yunshun - - family-names: Giner - given-names: Goknur - - family-names: Langaas - given-names: Mette - - family-names: Ferkingstad - given-names: Egil - - family-names: Davy - given-names: Marcus - - family-names: Pepin - given-names: Francois - - family-names: Choi - given-names: Dongseok - - family-names: Law - given-names: Charity - - family-names: Li - given-names: Mengbo - - family-names: Chen - given-names: Lizhong + email: smyth@wehi.edu.au + orcid: https://orcid.org/0000-0001-9221-2892 year: '2026' doi: 10.18129/B9.bioc.limma - type: software @@ -344,10 +303,13 @@ references: notes: Imports authors: - name: R Core Team + website: https://ror.org/02zz1nj61 institution: name: R Foundation for Statistical Computing + website: https://ror.org/05qewa988 address: Vienna, Austria year: '2026' + doi: 10.32614/R.manuals - type: software title: options abstract: 'options: Simple, Consistent Package Options' @@ -470,10 +432,13 @@ references: notes: Imports authors: - name: R Core Team + website: https://ror.org/02zz1nj61 institution: name: R Foundation for Statistical Computing + website: https://ror.org/05qewa988 address: Vienna, Austria year: '2026' + doi: 10.32614/R.manuals - type: software title: stringr abstract: 'stringr: Simple, Consistent Wrappers for Common String Operations' @@ -584,7 +549,7 @@ references: doi: 10.32614/CRAN.package.broom - type: software title: cffr - abstract: 'cffr: Generate Citation File Format (''cff'') Metadata for R Packages' + abstract: 'cffr: Generate Citation File Format (''CFF'') Metadata for R Packages' notes: Suggests url: https://docs.ropensci.org/cffr/ repository: https://CRAN.R-project.org/package=cffr @@ -961,9 +926,12 @@ references: abstract: 'UpSetR: A More Scalable Alternative to Venn and Euler Diagrams for Visualizing Intersecting Sets' notes: Suggests - url: http://github.com/hms-dbmi/UpSetR + url: https://github.com/hms-dbmi/UpSetR repository: https://CRAN.R-project.org/package=UpSetR authors: + - family-names: Conway + given-names: Jake + email: jake.conway@pathai.com - family-names: Gehlenborg given-names: Nils email: nils@hms.harvard.edu diff --git a/code/MOSuite/DESCRIPTION b/code/MOSuite/DESCRIPTION index 9f48d10..8f728ed 100644 --- a/code/MOSuite/DESCRIPTION +++ b/code/MOSuite/DESCRIPTION @@ -1,6 +1,6 @@ Package: MOSuite Title: R package for differential multi-omics analysis -Version: 0.3.1 +Version: 0.3.2 Authors@R: c( person("Kelly", "Sovacool", , "kelly.sovacool@nih.gov", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3283-829X")), @@ -29,7 +29,7 @@ Imports: DESeq2, dplyr, edgeR, - ggplot2 (< 4.0.0), + ggplot2, glue, htmlwidgets, jsonlite, diff --git a/code/MOSuite/NEWS.md b/code/MOSuite/NEWS.md index 24d05da..a1c1f01 100644 --- a/code/MOSuite/NEWS.md +++ b/code/MOSuite/NEWS.md @@ -1,8 +1,12 @@ +## 0.3.2 + +- Support ggplot2 v4.0.0 for correlation heatmap plots in `filter_counts()`, `normalize_counts()`, and `batch_correct_counts()`. (#205, @copilot, @kelly-sovacool) + ## MOSuite 0.3.1 - Fix recursion error in `plot_venn_diagram()`. (#188, @kelly-sovacool) - Fix S7 dispatch argument mismatch in `plot_read_depth()` and `plot_histogram()`. (#200, @copilot, @kelly-sovacool) -- Fix crash in `remove_low_count_genes()` when `use_group_based_filtering = TRUE`. (#200, @copilot, @kelly-sovacool) +- Fix bug in `remove_low_count_genes()` when `use_group_based_filtering = TRUE`. (#200, @copilot, @kelly-sovacool) - Fix color palette selection to fall back to random colors with a message when the number of categories exceeds the palette maximum. (#204, @copilot, @kelly-sovacool) - Update S7 class, generic, and method documentation to use roxygen2 v8.0.0. (#206, #212, @copilot, @kelly-sovacool) - A docker container with only MOSuite's dependencies, not MOSuite itself, is now available: . (#209, @kelly-sovacool) diff --git a/code/MOSuite/R/batch-correction.R b/code/MOSuite/R/batch-correction.R index 7e18183..bcbf63e 100644 --- a/code/MOSuite/R/batch-correction.R +++ b/code/MOSuite/R/batch-correction.R @@ -158,6 +158,7 @@ batch_correct_counts <- function( group_colname = batch_colname, label_colname = label_colname, color_values = colors_for_plots, + print_plots = FALSE, save_plots = FALSE ) + ggplot2::labs(caption = "batch-corrected counts") @@ -181,8 +182,7 @@ batch_correct_counts <- function( group_colname = batch_colname, label_colname = label_colname, color_values = colors_for_plots - ) + - ggplot2::labs(caption = "batch-corrected counts") + ) print_or_save_plot( pca_plot, @@ -200,7 +200,8 @@ batch_correct_counts <- function( corHM_plot, filename = file.path(plots_subdir, "corr_heatmap.png"), print_plots = print_plots, - save_plots = save_plots + save_plots = save_plots, + caption = "batch-corrected counts" ) } diff --git a/code/MOSuite/R/filter.R b/code/MOSuite/R/filter.R index 7e0625f..b520ef1 100644 --- a/code/MOSuite/R/filter.R +++ b/code/MOSuite/R/filter.R @@ -216,6 +216,7 @@ filter_counts <- function( label_font_size = label_font_size, label_offset_y_ = label_offset_y_, label_offset_x_ = label_offset_x_, + print_plots = FALSE, save_plots = FALSE ) + ggplot2::labs(caption = "filtered counts") @@ -246,13 +247,13 @@ filter_counts <- function( label_colname = label_colname, group_colname = group_colname, color_values = colors_for_plots - ) + - ggplot2::labs(caption = "filtered counts") + ) print_or_save_plot( corHM, filename = file.path(plots_subdir, "corr_heatmap.png"), print_plots = print_plots, - save_plots = save_plots + save_plots = save_plots, + caption = "filtered counts" ) } diff --git a/code/MOSuite/R/normalize.R b/code/MOSuite/R/normalize.R index e6ebd76..4823217 100644 --- a/code/MOSuite/R/normalize.R +++ b/code/MOSuite/R/normalize.R @@ -129,6 +129,7 @@ normalize_counts <- function( label_font_size = label_font_size, label_offset_y_ = label_offset_y_, label_offset_x_ = label_offset_x_, + print_plots = FALSE, save_plots = FALSE ) + ggplot2::labs(caption = "normalized counts") @@ -156,13 +157,13 @@ normalize_counts <- function( group_colname = group_colname, label_colname = label_colname, color_values = colors_for_plots - ) + - ggplot2::labs(caption = "normalized counts") + ) print_or_save_plot( corHM_plot, filename = file.path(plots_subdir, "corr_heatmap.png"), print_plots = print_plots, - save_plots = save_plots + save_plots = save_plots, + caption = "normalized counts" ) } diff --git a/code/MOSuite/R/plots.R b/code/MOSuite/R/plots.R index c814b78..1d5770e 100644 --- a/code/MOSuite/R/plots.R +++ b/code/MOSuite/R/plots.R @@ -3,12 +3,15 @@ #' If `save_plots` is `TRUE`, the plot will be saved as an image to the path at #' `file.path(plots_dir, filename)`. #' If `plot_obj` is a ggplot, `ggplot2::ggsave()` is used to save the image. -#' Otherwise, `graphics_device` is used (`grDevice::png()` by default). +#' Otherwise, `graphics_device` is used (`grDevices::png()` by default). #' #' @inheritParams option_params #' @param plot_obj plot object (e.g. ggplot, ComplexHeatmap...) #' @param filename name of the output file. will be joined with the `plots_dir` option. -#' @param graphics_device Default: `grDevice::png()`. Only used if the plot is not a ggplot. +#' @param graphics_device Default: `grDevices::png()`. Only used if the plot is not a ggplot. +#' @param caption optional caption text to add to the plot. For ggplot objects, this is +#' added via `ggplot2::labs(caption = caption)`. For `ComplexHeatmap` objects, the +#' caption is rendered at the bottom of the graphics device using `grid::grid.text()`. #' @param ... arguments forwarded to `ggplot2::ggsave()` #' #' @return invisibly returns the path where the plot image was saved to the disk @@ -22,10 +25,30 @@ print_or_save_plot <- function( save_plots = options::opt("save_plots"), plots_dir = options::opt("plots_dir"), graphics_device = grDevices::png, + caption = NULL, ... ) { + draw_heatmap_with_caption <- function(hm) { + ComplexHeatmap::draw(hm) + if (!is.null(caption)) { + grid::grid.text( + caption, + x = grid::unit(0.5, "npc"), + y = grid::unit(2, "mm"), + just = "bottom", + gp = grid::gpar(fontsize = 9, col = "grey40") + ) + } + } + if (!is.null(caption) && inherits(plot_obj, "ggplot")) { + plot_obj <- plot_obj + ggplot2::labs(caption = caption) + } if (isTRUE(print_plots)) { - print(plot_obj) + if (inherits(plot_obj, c("Heatmap", "HeatmapList"))) { + draw_heatmap_with_caption(plot_obj) + } else { + print(plot_obj) + } } if (isTRUE(save_plots)) { # create output directory if it doesn't exist @@ -42,10 +65,14 @@ print_or_save_plot <- function( ggplot2::ggsave(filename = filename, plot = plot_obj, ...) } else if (inherits(plot_obj, "htmlwidget")) { htmlwidgets::saveWidget(plot_obj, filename, ...) + } else if (inherits(plot_obj, c("Heatmap", "HeatmapList"))) { + graphics_device(file = filename) + on.exit(grDevices::dev.off(), add = TRUE) + draw_heatmap_with_caption(plot_obj) } else { graphics_device(file = filename) + on.exit(grDevices::dev.off(), add = TRUE) plot(plot_obj) - grDevices::dev.off() } } return(invisible(filename)) diff --git a/code/MOSuite/codemeta.json b/code/MOSuite/codemeta.json index cb4a191..5cf8660 100644 --- a/code/MOSuite/codemeta.json +++ b/code/MOSuite/codemeta.json @@ -32,5 +32,5 @@ "license": "https://spdx.org/licenses/MIT", "name": "MOSuite: R package for differential multi-omics analysis", "url": "https://ccbr.github.io/MOSuite/", - "version": "0.3.1" + "version": "0.3.1.9001" } diff --git a/code/MOSuite/docker/Dockerfile b/code/MOSuite/docker/Dockerfile index a97d70b..73385eb 100644 --- a/code/MOSuite/docker/Dockerfile +++ b/code/MOSuite/docker/Dockerfile @@ -21,8 +21,8 @@ RUN mamba install -y -c conda-forge \ r-colorspace \ r-dendextend \ r-devtools \ + r-ggplot2 \ r-dplyr \ - "r-ggplot2 <4.0.0" \ r-ggrepel \ r-gridExtra \ r-here \ diff --git a/code/MOSuite/docker/Dockerfile_minimal b/code/MOSuite/docker/Dockerfile_minimal index eb91b2c..014755d 100644 --- a/code/MOSuite/docker/Dockerfile_minimal +++ b/code/MOSuite/docker/Dockerfile_minimal @@ -21,8 +21,8 @@ RUN mamba install -y -c conda-forge \ r-colorspace \ r-dendextend \ r-devtools \ + r-ggplot2 \ r-dplyr \ - "r-ggplot2 <4.0.0" \ r-ggrepel \ r-gridExtra \ r-here \ diff --git a/code/MOSuite/inst/extdata/galaxy/galaxy.R b/code/MOSuite/inst/extdata/galaxy/galaxy.R index 3d92b9f..33df8ce 100644 --- a/code/MOSuite/inst/extdata/galaxy/galaxy.R +++ b/code/MOSuite/inst/extdata/galaxy/galaxy.R @@ -135,9 +135,7 @@ update_function_template <- function( arg_name <- template[[arg_type]][[i]]$key if (arg_name %in% names(func_meta$args)) { arg_meta <- template[[arg_type]][[i]] - arg_meta$description <- safe_rd_to_md( - func_meta$args[[arg_name]]$description - ) + arg_meta$description <- safe_rd_to_md(func_meta$args[[arg_name]]$description) arg_meta$defaultValue <- func_meta$args[[arg_name]]$defaultValue args_in_template <- c(args_in_template, arg_name) new_template[[arg_type]][[ diff --git a/code/MOSuite/man/print_or_save_plot.Rd b/code/MOSuite/man/print_or_save_plot.Rd index 4d22ef1..450ea1d 100644 --- a/code/MOSuite/man/print_or_save_plot.Rd +++ b/code/MOSuite/man/print_or_save_plot.Rd @@ -11,6 +11,7 @@ print_or_save_plot( save_plots = options::opt("save_plots"), plots_dir = options::opt("plots_dir"), graphics_device = grDevices::png, + caption = NULL, ... ) } @@ -25,7 +26,11 @@ print_or_save_plot( \item{plots_dir}{Path where plots are saved when \code{moo_save_plots} is \code{TRUE} (Defaults to \code{"figures/"}, overwritable using option 'moo_plots_dir' or environment variable 'MOO_PLOTS_DIR')} -\item{graphics_device}{Default: \code{grDevice::png()}. Only used if the plot is not a ggplot.} +\item{graphics_device}{Default: \code{grDevices::png()}. Only used if the plot is not a ggplot.} + +\item{caption}{optional caption text to add to the plot. For ggplot objects, this is +added via \code{ggplot2::labs(caption = caption)}. For \code{ComplexHeatmap} objects, the +caption is rendered at the bottom of the graphics device using \code{grid::grid.text()}.} \item{...}{arguments forwarded to \code{ggplot2::ggsave()}} } @@ -36,7 +41,7 @@ invisibly returns the path where the plot image was saved to the disk If \code{save_plots} is \code{TRUE}, the plot will be saved as an image to the path at \code{file.path(plots_dir, filename)}. If \code{plot_obj} is a ggplot, \code{ggplot2::ggsave()} is used to save the image. -Otherwise, \code{graphics_device} is used (\code{grDevice::png()} by default). +Otherwise, \code{graphics_device} is used (\code{grDevices::png()} by default). } \seealso{ Other plotters: diff --git a/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R b/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R index 6f351ca..a115797 100644 --- a/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R +++ b/code/MOSuite/tests/testthat/test-plot_volcano_enhanced.R @@ -1,4 +1,5 @@ test_that("plot_volcano_enhanced works on nidap dataset", { + skip_on_ci() expect_snapshot( df_volc_enh <- plot_volcano_enhanced( nidap_deg_analysis, diff --git a/code/MOSuite/tests/testthat/test-plots.R b/code/MOSuite/tests/testthat/test-plots.R index 43ada4f..bb965f3 100644 --- a/code/MOSuite/tests/testthat/test-plots.R +++ b/code/MOSuite/tests/testthat/test-plots.R @@ -1,6 +1,7 @@ set.seed(20250225) -test_that("save_or_print_plot works for ComplexHeatmap", { - p <- plot_corr_heatmap( + +corr_heatmap_fixture <- function() { + plot_corr_heatmap( nidap_filtered_counts |> as.data.frame(), sample_metadata = as.data.frame(nidap_sample_metadata), @@ -23,7 +24,59 @@ test_that("save_or_print_plot works for ComplexHeatmap", { "#878500" ) ) - skip() +} + +test_that("print_or_save_plot saves ComplexHeatmap to disk without error", { + p <- corr_heatmap_fixture() + outfile <- tempfile(fileext = ".png") + result <- print_or_save_plot( + p, + filename = outfile, + print_plots = FALSE, + save_plots = TRUE, + plots_dir = "", + caption = "filtered counts" + ) + expect_equal(result, outfile) + expect_true(file.exists(outfile)) + expect_gt(file.size(outfile), 0) +}) + +test_that("print_or_save_plot saves ggplot without error", { + p <- plot_read_depth(nidap_clean_raw_counts) + outfile <- tempfile(fileext = ".png") + result <- print_or_save_plot( + p, + filename = outfile, + print_plots = FALSE, + save_plots = TRUE, + plots_dir = "", + caption = "normalized counts" + ) + expect_equal(result, outfile) + expect_true(file.exists(outfile)) + expect_gt(file.size(outfile), 0) +}) + +test_that("print_or_save_plot prints ComplexHeatmap with caption without error", { + p <- corr_heatmap_fixture() + outfile <- tempfile(fileext = ".png") + withr::with_png(outfile, { + result <- print_or_save_plot( + p, + filename = outfile, + print_plots = TRUE, + save_plots = FALSE, + plots_dir = "", + caption = "batch-corrected counts" + ) + }) + expect_equal(result, outfile) +}) + +test_that("save_or_print_plot works for ComplexHeatmap", { + p <- corr_heatmap_fixture() + skip_on_ci() expect_snapshot_file( print_or_save_plot( p, @@ -37,7 +90,7 @@ test_that("save_or_print_plot works for ComplexHeatmap", { }) test_that("save_or_print_plot works for ggplot", { p <- plot_read_depth(nidap_clean_raw_counts) - skip() + skip_on_ci() expect_snapshot_file( print_or_save_plot( p,