mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge remote-tracking branch 'upstream/release_25.1' into merge_25.1_into_dev_de
This commit is contained in:
@@ -8,6 +8,7 @@ on:
|
||||
- 'release_*'
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -22,7 +23,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: dev
|
||||
sparse-checkout: |
|
||||
lib/galaxy/version.py
|
||||
|
||||
@@ -35,7 +35,36 @@ jobs:
|
||||
echo "version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Galaxy release version: ${RELEASE_VERSION}"
|
||||
|
||||
- name: Check if we should update draft
|
||||
id: check-draft
|
||||
run: |
|
||||
VERSION="${{ inputs.version || steps.galaxy-version.outputs.version }}"
|
||||
|
||||
# Check if this version is already published
|
||||
PUBLISHED=$(gh release view "v${VERSION}" --json isDraft --jq '.isDraft' 2>/dev/null || echo "not_found")
|
||||
|
||||
if [ "$PUBLISHED" = "false" ]; then
|
||||
echo "Published release v${VERSION} already exists. Skipping."
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if a draft exists for a DIFFERENT version (only one draft at a time)
|
||||
EXISTING_DRAFT=$(gh release list --limit 50 --json tagName,isDraft --jq '.[] | select(.isDraft == true) | .tagName' | head -1)
|
||||
|
||||
if [ -n "$EXISTING_DRAFT" ] && [ "$EXISTING_DRAFT" != "v${VERSION}" ]; then
|
||||
echo "Draft release $EXISTING_DRAFT already exists (for different version). Skipping v${VERSION}."
|
||||
echo "Only one draft release at a time is supported."
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Proceeding with draft for v${VERSION}."
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
if: steps.check-draft.outputs.skip == 'false'
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
version: ${{ steps.galaxy-version.outputs.version }}
|
||||
|
||||
@@ -137,6 +137,8 @@ class DatasetCollectionManager:
|
||||
collection_type_description = structure.collection_type_description
|
||||
dataset_collection = DatasetCollection(populated=False)
|
||||
dataset_collection.collection_type = collection_type_description.collection_type
|
||||
# Preserve column_definitions from input structure for sample sheets
|
||||
dataset_collection.column_definitions = structure.column_definitions
|
||||
elements = []
|
||||
for index, (identifier, substructure) in enumerate(structure.children):
|
||||
# TODO: Open question - populate these now or later?
|
||||
|
||||
@@ -64,12 +64,15 @@ class UninitializedTree(BaseTree):
|
||||
class Tree(BaseTree):
|
||||
children_known = True
|
||||
|
||||
def __init__(self, children, collection_type_description, when_values=None, columns_metadata=None):
|
||||
def __init__(
|
||||
self, children, collection_type_description, when_values=None, columns_metadata=None, column_definitions=None
|
||||
):
|
||||
super().__init__(collection_type_description)
|
||||
self.children = children
|
||||
self.when_values = when_values
|
||||
# columns_metadata is a dict mapping element_identifier to columns data
|
||||
self.columns_metadata = columns_metadata or {}
|
||||
self.column_definitions = column_definitions
|
||||
|
||||
@staticmethod
|
||||
def for_dataset_collection(dataset_collection, collection_type_description):
|
||||
@@ -90,7 +93,12 @@ class Tree(BaseTree):
|
||||
# Capture columns metadata from sample sheet collections
|
||||
if element.columns is not None:
|
||||
columns_metadata[element.element_identifier] = element.columns
|
||||
return Tree(children, collection_type_description, columns_metadata=columns_metadata)
|
||||
return Tree(
|
||||
children,
|
||||
collection_type_description,
|
||||
columns_metadata=columns_metadata,
|
||||
column_definitions=dataset_collection.column_definitions,
|
||||
)
|
||||
|
||||
def walk_collections(self, hdca_dict):
|
||||
return self._walk_collections(dict_map(lambda hdca: hdca.collection, hdca_dict))
|
||||
@@ -148,12 +156,22 @@ class Tree(BaseTree):
|
||||
for identifier, structure in self.children:
|
||||
new_children.append((identifier, structure.multiply(other_structure)))
|
||||
|
||||
# Preserve columns_metadata when multiplying
|
||||
return Tree(new_children, new_collection_type, columns_metadata=self.columns_metadata.copy())
|
||||
# Preserve columns_metadata and column_definitions when multiplying
|
||||
return Tree(
|
||||
new_children,
|
||||
new_collection_type,
|
||||
columns_metadata=self.columns_metadata.copy(),
|
||||
column_definitions=self.column_definitions,
|
||||
)
|
||||
|
||||
def clone(self):
|
||||
cloned_children = [(_[0], _[1].clone()) for _ in self.children]
|
||||
return Tree(cloned_children, self.collection_type_description, columns_metadata=self.columns_metadata.copy())
|
||||
return Tree(
|
||||
cloned_children,
|
||||
self.collection_type_description,
|
||||
columns_metadata=self.columns_metadata.copy(),
|
||||
column_definitions=self.column_definitions,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Tree[collection_type={self.collection_type_description},children=({','.join(f'{identifier_and_element[0]}={identifier_and_element[1]}' for identifier_and_element in self.children)})]"
|
||||
|
||||
@@ -1029,13 +1029,6 @@ class InputModule(WorkflowModule):
|
||||
step_outputs["input_ds_copy"] = new_hdca
|
||||
else:
|
||||
raise Exception("Unknown history content encountered")
|
||||
# If coming from UI - we haven't registered invocation inputs yet,
|
||||
# so do that now so dependent steps can be recalculated. In the future
|
||||
# everything should come in from the API and this can be eliminated.
|
||||
if not invocation.has_input_for_step(step.id):
|
||||
content = next(iter(step_outputs.values()))
|
||||
if content and content is not NO_REPLACEMENT:
|
||||
invocation.add_input(content, step.id)
|
||||
progress.set_outputs_for_input(invocation_step, step_outputs)
|
||||
return None
|
||||
|
||||
|
||||
@@ -623,6 +623,12 @@ class WorkflowProgress:
|
||||
|
||||
if step.label and step.type == "parameter_input" and "output" in outputs:
|
||||
self.runtime_replacements[step.label] = str(outputs["output"])
|
||||
invocation = invocation_step.workflow_invocation
|
||||
if not invocation.has_input_for_step(step.id):
|
||||
content = outputs.get("output", NO_REPLACEMENT)
|
||||
if content is not NO_REPLACEMENT:
|
||||
log.info("ADDING INPUT FOR STEP %s: %s", step.id, content, exc_info=True)
|
||||
invocation.add_input(content, step.id)
|
||||
self.set_step_outputs(invocation_step, outputs, already_persisted=already_persisted)
|
||||
|
||||
def effective_replacement_dict(self):
|
||||
|
||||
@@ -481,6 +481,7 @@ class TestDatasetCollectionsApi(ApiTestCase):
|
||||
collection_details = self.dataset_populator.get_history_collection_details(
|
||||
history_id, content_id=output_collection["id"]
|
||||
)
|
||||
assert collection_details["column_definitions"] == sample_sheet["column_definitions"]
|
||||
|
||||
# Verify that the output collection preserved the columns metadata
|
||||
output_elements = collection_details["elements"]
|
||||
|
||||
@@ -5622,6 +5622,98 @@ test_data:
|
||||
messages = subworkflow_invocation.get("messages", [])
|
||||
assert len(messages) == 0, f"Expected no error messages, got: {messages}"
|
||||
|
||||
def test_run_subworkflow_with_boolean_parameter_in_when_condition(self):
|
||||
"""Test boolean false parameter passed to subworkflow with when condition.
|
||||
|
||||
This test verifies that boolean parameters (especially false) are properly
|
||||
passed from parent to subworkflow when the subworkflow has:
|
||||
1. Delayed scheduling (via $link)
|
||||
2. A when condition that uses the boolean parameter
|
||||
|
||||
Previously, false values were converted to None in the when expression evaluation,
|
||||
causing "when_not_boolean" errors.
|
||||
"""
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
workflow = """
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
should_run:
|
||||
type: boolean
|
||||
some_file:
|
||||
type: data
|
||||
steps:
|
||||
nested_workflow:
|
||||
in:
|
||||
subworkflow_should_run: should_run
|
||||
subworkflow_file: some_file
|
||||
run:
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
subworkflow_should_run: boolean
|
||||
subworkflow_file: data
|
||||
steps:
|
||||
expression:
|
||||
tool_id: expression_forty_two
|
||||
state: {}
|
||||
conditional_step:
|
||||
tool_id: cheetah_casting
|
||||
in:
|
||||
subworkflow_should_run: subworkflow_should_run
|
||||
state:
|
||||
floattest: 3.14
|
||||
inttest:
|
||||
$link: expression/out1
|
||||
when: $(inputs.subworkflow_should_run)
|
||||
test_data:
|
||||
some_file:
|
||||
value: 1.bed
|
||||
type: File
|
||||
should_run:
|
||||
value: false
|
||||
type: raw
|
||||
"""
|
||||
summary = self._run_workflow(workflow, history_id=history_id, wait=True, assert_ok=True)
|
||||
|
||||
# Verify parent workflow executed successfully
|
||||
parent_invocation = self.workflow_populator.get_invocation(summary.invocation_id, step_details=True)
|
||||
assert parent_invocation["state"] == "scheduled"
|
||||
|
||||
# Find the subworkflow step and get its invocation
|
||||
subworkflow_step = None
|
||||
for step in parent_invocation["steps"]:
|
||||
if step.get("subworkflow_invocation_id"):
|
||||
subworkflow_step = step
|
||||
break
|
||||
|
||||
assert subworkflow_step is not None, "No subworkflow step found"
|
||||
subworkflow_invocation_id = subworkflow_step["subworkflow_invocation_id"]
|
||||
subworkflow_invocation = self.workflow_populator.get_invocation(
|
||||
subworkflow_invocation_id, step_details=True
|
||||
)
|
||||
|
||||
# The subworkflow should have succeeded
|
||||
assert (
|
||||
subworkflow_invocation["state"] == "scheduled"
|
||||
), f"Expected subworkflow to succeed, got state: {subworkflow_invocation['state']}"
|
||||
|
||||
# Should not have error messages (previously failed with "when_not_boolean")
|
||||
messages = subworkflow_invocation.get("messages", [])
|
||||
assert len(messages) == 0, f"Expected no error messages, got: {messages}"
|
||||
|
||||
# Find the conditional step in the subworkflow and verify it was skipped
|
||||
# (when condition was false, so step should not execute)
|
||||
conditional_step = None
|
||||
for step in subworkflow_invocation["steps"]:
|
||||
if step.get("workflow_step_label") == "conditional_step":
|
||||
conditional_step = step
|
||||
break
|
||||
|
||||
assert conditional_step is not None, "Conditional step not found in subworkflow"
|
||||
# The step should have been skipped because should_run=false
|
||||
assert len(conditional_step["jobs"]) == 0 or all(
|
||||
j["state"] == "skipped" for j in conditional_step["jobs"]
|
||||
), "Expected conditional step to be skipped when should_run=false"
|
||||
|
||||
def test_run_with_non_optional_data_unspecified_fails_invocation(self):
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
error = self._run_jobs(
|
||||
|
||||
@@ -247,6 +247,7 @@ class MockCollection:
|
||||
self.collection_type = collection_type
|
||||
self.elements = elements
|
||||
self.populated = True
|
||||
self.column_definitions = None
|
||||
|
||||
|
||||
class MockCollectionElement:
|
||||
|
||||
@@ -94,8 +94,7 @@ class TestWorkflowProgress(TestCase):
|
||||
workflow_invocation_step.state = "scheduled"
|
||||
workflow_invocation_step.workflow_step = self._step(i)
|
||||
assert step_id == self._step(i).id
|
||||
# workflow_invocation_step.workflow_invocation = self.invocation
|
||||
self.invocation.steps.append(workflow_invocation_step)
|
||||
workflow_invocation_step.workflow_invocation = self.invocation
|
||||
|
||||
workflow_invocation_step_state = model.WorkflowRequestStepState()
|
||||
workflow_invocation_step_state.workflow_step_id = step_id
|
||||
@@ -111,6 +110,7 @@ class TestWorkflowProgress(TestCase):
|
||||
else:
|
||||
workflow_invocation_step = model.WorkflowInvocationStep()
|
||||
workflow_invocation_step.workflow_step = self._step(index)
|
||||
workflow_invocation_step.workflow_invocation = self.invocation
|
||||
return workflow_invocation_step
|
||||
|
||||
def test_connect_data_input(self):
|
||||
@@ -211,6 +211,7 @@ class TestWorkflowProgress(TestCase):
|
||||
subworkflow_invocation_step.workflow_step_id = subworkflow_input_step.id
|
||||
subworkflow_invocation_step.state = "new"
|
||||
subworkflow_invocation_step.workflow_step = subworkflow_input_step
|
||||
subworkflow_invocation_step.workflow_invocation = subworkflow_invocation
|
||||
|
||||
subworkflow_progress.set_outputs_for_input(subworkflow_invocation_step)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user