diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 9c17fd6..106b2f6 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -20,33 +20,32 @@ jobs: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - - name: Set up Python 3.14 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + - name: Setup Conda + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 with: - python-version: "3.14" + miniforge-variant: Miniforge3 + auto-activate: true + activate-environment: base + conda-remove-defaults: true + channels: conda-forge,bioconda + channel-priority: strict - - name: Install testing dependencies + - name: Install conda-build + shell: bash -l {0} run: | - python -m pip install --upgrade pip - python -m pip install -e '.[testing]' + conda install -y \ + --override-channels \ + -c conda-forge \ + conda-build - - name: Run regression tests + - name: Build and run regression tests + shell: bash -l {0} run: | - python -m pytest tests \ - -m regression \ - -vv \ - --durations=20 \ - --basetemp=.testdata - - - name: Upload regression artefacts on failure - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: pr-regression-failure - path: .testdata/ - if-no-files-found: ignore - include-hidden-files: true - retention-days: 7 + conda build conda-recipe \ + --override-channels \ + -c conda-forge \ + -c bioconda \ + --no-anaconda-upload docs: name: Docs diff --git a/.github/workflows/regression-tests.yaml b/.github/workflows/regression-tests.yaml index 394132e..d046ca2 100644 --- a/.github/workflows/regression-tests.yaml +++ b/.github/workflows/regression-tests.yaml @@ -25,30 +25,29 @@ jobs: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - - name: Set up Python 3.14 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + - name: Setup Conda + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 with: - python-version: "3.14" - - - name: Install testing dependencies + miniforge-variant: Miniforge3 + auto-activate: true + activate-environment: base + conda-remove-defaults: true + channels: conda-forge,bioconda + channel-priority: strict + + - name: Install conda-build + shell: bash -l {0} run: | - python -m pip install --upgrade pip - python -m pip install -e '.[testing]' + conda install -y \ + --override-channels \ + -c conda-forge \ + conda-build - - name: Run regression tests + - name: Build and run regression tests + shell: bash -l {0} run: | - python -m pytest tests \ - -m regression \ - -vv \ - --durations=20 \ - --basetemp=.testdata - - - name: Upload regression artefacts on failure - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: sist-regression-failure - path: .testdata/ - if-no-files-found: ignore - include-hidden-files: true - retention-days: 7 \ No newline at end of file + conda build conda-recipe \ + --override-channels \ + -c conda-forge \ + -c bioconda \ + --no-anaconda-upload \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..6aec1ba --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,229 @@ +name: Release SIST + +on: + workflow_dispatch: + inputs: + version: + required: true + default: 'x.y.z' + +permissions: + contents: write + pull-requests: write + +jobs: + checks: + name: Version check + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + id: repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: 3.14.6 + + - name: Get latest release + id: latestreleased + run: | + PREVIOUS_VERSION=$(git tag --sort=-v:refname | head -n 1) + echo "release_tag=$PREVIOUS_VERSION" >> "$GITHUB_OUTPUT" + echo "$PREVIOUS_VERSION" + + - name: version comparison + id: compare + run: | + python -m pip install semver + + PREVIOUS_VERSION="${{ steps.latestreleased.outputs.release_tag }}" + NEW_VERSION="${{ github.event.inputs.version }}" + + if [ -z "$PREVIOUS_VERSION" ]; then + echo "No previous release found" + exit 0 + fi + + output=$(pysemver compare "$PREVIOUS_VERSION" "$NEW_VERSION") + + if [ "$output" -ge 0 ]; then + exit 1 + fi + + version: + name: prepare ${{ github.event.inputs.version }} + needs: checks + runs-on: ubuntu-24.04 + steps: + + - name: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Change version in repo and CITATION.cff + run: | + # Update Conda package version + sed -i \ + "s/{% set version = \".*\" %}/{% set version = \"${{ github.event.inputs.version }}\" %}/" \ + conda-recipe/meta.yaml + + # Update CITATION.cff version and date-released + if [ -f CITATION.cff ]; then + sed -i -E \ + "s/^(version:\s*).*/\1${{ github.event.inputs.version }}/" \ + CITATION.cff + + sed -i -E \ + "s/^(date-released:\s*).*/\1'$(date -u +%F)'/" \ + CITATION.cff + fi + + - name: send PR + id: pr_id + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + commit-message: Update version to ${{ github.event.inputs.version }} + branch: version-update + title: "Update to version ${{ github.event.inputs.version }}" + body: | + Update version + - Update the Conda recipe with new release + - Update CITATION.cff version & date-released + - Auto-generated by [CI] + committer: version-updater + author: version-updater + base: main + signoff: false + draft: false + + - name: auto approve review + uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0 + with: + pull-request-number: ${{ steps.pr_id.outputs.pull-request-number }} + review-message: "Auto approved version bump PR" + github-token: ${{ secrets.AUTO_PR_MERGE }} + + - name: merge PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.pr_id.outputs.pull-request-number }} + run: | + gh pr merge \ + --merge \ + --delete-branch \ + --auto \ + "$PR_NUMBER" + + while true; do + STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state') + + if [ "$STATE" = "MERGED" ]; then + echo "Version bump PR merged" + break + fi + + if [ "$STATE" = "CLOSED" ]; then + echo "Version bump PR was closed without merging" + exit 1 + fi + + sleep 10 + done + + tag: + name: tag release + needs: version + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: main + + - name: tag v${{ github.event.inputs.version }} + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git tag ${{ github.event.inputs.version }} + git push origin tag ${{ github.event.inputs.version }} + + release: + name: make github release + needs: tag + runs-on: ubuntu-24.04 + steps: + + - name: create release + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + with: + name: v${{ github.event.inputs.version }} + generate_release_notes: true + tag_name: ${{ github.event.inputs.version }} + + conda: + name: publish conda to anaconda.org + needs: [tag, release] + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.inputs.version }} + + - name: Setup Conda + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 + with: + miniforge-variant: Miniforge3 + python-version: "3.14" + auto-activate: true + activate-environment: base + conda-remove-defaults: true + channels: conda-forge,bioconda + channel-priority: strict + + - name: Install build tools + shell: bash -l {0} + run: | + conda install -y \ + --override-channels \ + -c conda-forge \ + conda-build \ + anaconda-client + + conda config --set anaconda_upload no + conda list conda-build + conda build --help + + - name: Build package + shell: bash -l {0} + run: | + PKG_PATH=$(conda build conda-recipe \ + --output \ + --override-channels \ + -c conda-forge \ + -c bioconda) + + echo "PKG_PATH=$PKG_PATH" >> "$GITHUB_ENV" + + conda build conda-recipe \ + --override-channels \ + -c conda-forge \ + -c bioconda \ + --no-anaconda-upload + + test -f "$PKG_PATH" + echo "Built: $PKG_PATH" + + - name: Upload to Anaconda + shell: bash -l {0} + env: + ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }} + run: | + anaconda \ + -t "$ANACONDA_API_TOKEN" \ + upload "$PKG_PATH" \ + --user CCPBioSim \ + --force \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..5ed8a20 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,36 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: SIST +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software + +authors: + +repository-code: 'https://github.com/CCPBioSim/SIST' +url: 'https://sist.readthedocs.io/en/latest/' + +abstract: >- + SIST (Stress-Induced Structural Transitions) calculates + stress-induced structural transition probabilities in + superhelical DNA, including strand separation, Z-DNA + formation, cruciform extrusion, and competition between + these transitions. + +keywords: + - DNA + - superhelical DNA + - DNA supercoiling + - strand separation + - Z-DNA + - cruciform extrusion + - structural transitions + - statistical mechanics + - biomolecular simulations + +license: MIT +version: 0.0.0 +date-released: '2026-08-14' \ No newline at end of file diff --git a/IR_finder.pl b/IR_finder.pl index 91bf088..adbf282 100755 --- a/IR_finder.pl +++ b/IR_finder.pl @@ -21,8 +21,7 @@ die $usage; } -#my $code = "irf305.macos.exe"; -my $code = "irf308.linux.exe"; +my $code = "irf"; my $temp = $ARGV[0]; #temperature my $shape = $ARGV[1]; #linear or circular @@ -110,7 +109,7 @@ sub get_result { my ($in_file,$shape) = @_; my $skip = 0; #PARAMETERS: Match: $match Mismatch: $mismatch Delta: $delta pm: $pm pi: $pi minscore:$minscore maxlength: $maxlength maxloop: $maxloop \n"; - system("./$code $in_file $match $mismatch $delta $pm $pi $minscore $maxlength $maxloop > /dev/null"); + system("$code $in_file $match $mismatch $delta $pm $pi $minscore $maxlength $maxloop > /dev/null"); my $num_files = `ls -l $in_file.$match.$mismatch.$delta.$pm.$pi.$minscore.$maxlength.$maxloop.*.txt.html | wc -l`; for(my $i = 1; $i <= $num_files; $i++) { my $out_file = "$in_file.$match.$mismatch.$delta.$pm.$pi.$minscore.$maxlength.$maxloop.$i.txt.html"; @@ -275,7 +274,7 @@ sub get_IR { my @la = split("--",$rrl[0]); my @ra = split("--",$rrl[1]); my $start = $la[0]; #start position of IR - my $end = $ra[1]; #end position of IR + my $end = $ra[1]; #end of IR my $s_loop = $la[1]+1; #start position of loop my $l_IR = $end-$start+1; #length of IR return ($l_loop,$s_loop,$start,$end,$l_IR); diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh new file mode 100755 index 0000000..01991f8 --- /dev/null +++ b/conda-recipe/build.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euxo pipefail + +make -C trans_three clean +make -C trans_three + +make -C trans_compete clean +make -C trans_compete + +install -d "${PREFIX}/bin" +install -d "${PREFIX}/libexec/sist" +install -d "${PREFIX}/libexec/sist/trans_three" +install -d "${PREFIX}/libexec/sist/trans_compete" + +install -m 755 \ + master.pl \ + "${PREFIX}/libexec/sist/master.pl" + +install -m 755 \ + IR_finder.pl \ + "${PREFIX}/libexec/sist/IR_finder.pl" + +install -m 755 \ + trans_three/qsidd \ + "${PREFIX}/libexec/sist/trans_three/qsidd" + +install -m 755 \ + trans_compete/qsidd \ + "${PREFIX}/libexec/sist/trans_compete/qsidd" + +cat > "${PREFIX}/bin/sist" << 'EOF' +#!/usr/bin/env bash + +PREFIX="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +exec "${PREFIX}/bin/perl" \ + "${PREFIX}/libexec/sist/master.pl" \ + "$@" +EOF + +chmod 755 "${PREFIX}/bin/sist" \ No newline at end of file diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml new file mode 100644 index 0000000..8b610ca --- /dev/null +++ b/conda-recipe/meta.yaml @@ -0,0 +1,43 @@ +{% set name = "sist" %} +{% set version = "0.0.0" %} + +package: + name: {{ name }} + version: {{ version }} + +source: + path: .. + +build: + number: 0 + +requirements: + build: + - {{ compiler('cxx') }} + - make + + run: + - perl + - irf >=3.08,<3.09 + +test: + requires: + - python >=3.14 + - pytest >=9,<10 + + source_files: + - tests + - pyproject.toml + +about: + home: https://github.com/CCPBioSim/SIST + license: MIT + license_family: MIT + license_file: LICENSE + license_url: https://github.com/CCPBioSim/SIST/blob/main/LICENSE + summary: Stress-Induced Structural Transitions in superhelical DNA. + description: > + SIST calculates stress-induced structural transition probabilities in + superhelical DNA, including strand separation, Z-DNA formation, + cruciform extrusion, and competition between these transitions. + dev_url: https://github.com/CCPBioSim/SIST \ No newline at end of file diff --git a/conda-recipe/run_test.sh b/conda-recipe/run_test.sh new file mode 100755 index 0000000..f6bace2 --- /dev/null +++ b/conda-recipe/run_test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +command -v sist +command -v irf +command -v perl +command -v python +command -v pytest + +python -m pytest tests -vv \ No newline at end of file diff --git a/irf308.linux.exe b/irf308.linux.exe deleted file mode 100755 index ed62440..0000000 Binary files a/irf308.linux.exe and /dev/null differ diff --git a/master.pl b/master.pl index dda6dfa..61d852b 100755 --- a/master.pl +++ b/master.pl @@ -5,6 +5,7 @@ use strict; use warnings; use Getopt::Long; +use FindBin qw($RealBin); my ($file,$trans, $out_file); my $temp = 310; my $sig = 0.06; @@ -72,8 +73,8 @@ my @name = split("/",$file); -my $single_exe = "trans_three/qsidd"; -my $compete_exe = "trans_compete/qsidd"; +my $single_exe = "$RealBin/trans_three/qsidd"; +my $compete_exe = "$RealBin/trans_compete/qsidd"; my $output_IR; if ($trans eq "M") { @@ -85,7 +86,7 @@ $output_IR = `$single_exe $b $p $r $c -T $temp -s $sig -i $salt -t $theta -Z -f $file`; } if ($trans eq "C" or $trans eq "A") { - my $code_IR = "IR_finder.pl"; + my $code_IR = "$RealBin/IR_finder.pl"; my $IR_results = `perl $code_IR $temp $shape $file`; #run IR_finder if($trans eq "C") { #run single transition algorithm for cruciforms diff --git a/tests/conftest.py b/tests/conftest.py index ad3c98f..f5eea65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,14 +66,10 @@ def require_success( ) -@pytest.fixture(scope="session") -def built_sist(tmp_path_factory: pytest.TempPathFactory) -> Path: - """ - Copy and build SIST in a temporary directory. - - This ensures the tests use executables built from the maintained source - without modifying the repository working tree. - """ +def build_sist( + tmp_path_factory: pytest.TempPathFactory, +) -> Path: + """Copy and build SIST in a temporary directory.""" build_root = tmp_path_factory.mktemp("sist-build") working_copy = build_root / "SIST" @@ -121,39 +117,73 @@ def built_sist(tmp_path_factory: pytest.TempPathFactory) -> Path: return working_copy +@pytest.fixture(scope="session") +def sist_command( + tmp_path_factory: pytest.TempPathFactory, +) -> list[str]: + """ + Return the SIST command under test. + + During conda-build testing, use the installed package. Otherwise build + and test the maintained source tree. + """ + + if os.environ.get("CONDA_BUILD_STATE") == "TEST": + executable = shutil.which("sist") + + if executable is None: + pytest.fail( + "The installed sist command was not found in PATH", + pytrace=False, + ) + + return [executable] + + built_sist = build_sist(tmp_path_factory) + + return [ + "perl", + str(built_sist / "master.pl"), + ] + + def run_sist_calculation( - built_sist: Path, + sist_command: list[str], + tmp_path_factory: pytest.TempPathFactory, *, name: str, algorithm: str, ) -> SistRun: """Run one SIST calculation using the regression test sequence.""" - source_input = built_sist / "tests" / "data" / "pbr322.toy.fa" - runtime_input = built_sist / "pbr322.toy.fa" + runtime_directory = tmp_path_factory.mktemp(f"sist-{name}") - shutil.copy2(source_input, runtime_input) + source_input = ( + REPOSITORY_ROOT + / "tests" + / "data" + / "pbr322.toy.fa" + ) + runtime_input = runtime_directory / "pbr322.toy.fa" - output_directory = built_sist / "test-results" - output_directory.mkdir(exist_ok=True) + shutil.copy2(source_input, runtime_input) - output_path = output_directory / f"{name}.txt" + output_path = runtime_directory / f"{name}.txt" result = run_command( [ - "perl", - "master.pl", + *sist_command, "-f", runtime_input.name, "-a", algorithm, "-o", - str(output_path.relative_to(built_sist)), + output_path.name, "-b", "-p", "-r", ], - cwd=built_sist, + cwd=runtime_directory, ) return SistRun( @@ -165,11 +195,15 @@ def run_sist_calculation( @pytest.fixture(scope="session") -def competition_run(built_sist: Path) -> SistRun: +def competition_run( + sist_command: list[str], + tmp_path_factory: pytest.TempPathFactory, +) -> SistRun: """Run the SIST competition calculation once.""" return run_sist_calculation( - built_sist, + sist_command, + tmp_path_factory, name="competition", algorithm="A", ) @@ -181,14 +215,16 @@ def competition_run(built_sist: Path) -> SistRun: ) def transition_run( request: pytest.FixtureRequest, - built_sist: Path, + sist_command: list[str], + tmp_path_factory: pytest.TempPathFactory, ) -> SistRun: """Run each supported individual SIST transition calculation once.""" name, algorithm = request.param return run_sist_calculation( - built_sist, + sist_command, + tmp_path_factory, name=name, algorithm=algorithm, ) diff --git a/trans_compete/Makefile b/trans_compete/Makefile index c991d8b..703b15c 100644 --- a/trans_compete/Makefile +++ b/trans_compete/Makefile @@ -27,13 +27,13 @@ default: make gcc $(APP): $(OBJ) $(OBJECTS) - $(CC) -o $(APP) $(CFLAGS) $(OBJ) $(OBJECTS) -lm + $(CC) $(LDFLAGS) -o $(APP) $(CFLAGS) $(OBJ) $(OBJECTS) -lm clean: rm -f *.o $(APP) depend: $(OBJECTS:.o=.cpp) - gcc -MM $^ > $@ + $(CXX) -MM $^ > $@ test: ./qsidd example.fasta @@ -52,7 +52,7 @@ tar: ################# gcc: - make $(APP) CC="g++" CPPFLAGS="-O2 -Wall" + make $(APP) CC="$(CXX)" CPPFLAGS="-O2 -Wall" ################### diff --git a/trans_three/Makefile b/trans_three/Makefile index dc336c5..06b0afe 100644 --- a/trans_three/Makefile +++ b/trans_three/Makefile @@ -16,6 +16,7 @@ APP = qsidd SRC = qsidd.c OBJ = qsidd.o + DATE = $(shell date +\%Y-\%m-\%d) ########### @@ -26,13 +27,13 @@ default: make gcc $(APP): $(OBJ) $(OBJECTS) - $(CC) -o $(APP) $(CFLAGS) $(OBJ) $(OBJECTS) -lm + $(CC) $(LDFLAGS) -o $(APP) $(CFLAGS) $(OBJ) $(OBJECTS) -lm clean: rm -f *.o $(APP) depend: $(OBJECTS:.o=.cpp) - gcc -MM $^ > $@ + $(CXX) -MM $^ > $@ test: ./qsidd example.fasta @@ -51,7 +52,7 @@ tar: ################# gcc: - make $(APP) CC="g++" CPPFLAGS="-O2 -Wall " + make $(APP) CC="$(CXX)" CPPFLAGS="-O2 -Wall " ###################