Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6352f73
build(deps): bump tower-java-sdk to 1.230.0 [COMP-2645]
cristianrcv Oct 2, 2026
0038894
fix(launch): keep waiting through failed status polls after submit [C…
cristianrcv Oct 2, 2026
d3f2375
fix(build): allow PATCH requests on Java 25 [COMP-2645]
cristianrcv Oct 2, 2026
cb7e4e0
fix(build): open the JDK packages for PATCH in the native image too […
cristianrcv Oct 2, 2026
11fc328
feat(service-accounts): add service account commands [COMP-2645]
cristianrcv Oct 2, 2026
38c04da
feat(actions): add and update pipeline-status actions [COMP-2645]
cristianrcv Oct 2, 2026
09b0c6c
feat(actions): add 'actions triggers list' and 'actions triggers view…
cristianrcv Oct 2, 2026
352e0ca
feat(launch): add --fusion-version [COMP-2645]
cristianrcv Oct 2, 2026
b953816
feat(info): list Fusion versions from the compatibility catalog in 't…
cristianrcv Oct 2, 2026
b255283
feat(credentials): add credentials federation-setup command [COMP-2645]
cristianrcv Oct 2, 2026
e288e68
feat(compute-envs): add --key-vault-url to azure-cloud [COMP-2645]
cristianrcv Oct 2, 2026
060c052
feat(compute-envs): add --billing-export-table to google-cloud [COMP-…
cristianrcv Oct 2, 2026
b83a26d
feat(agents): set an agent's service account and GitHub App credentia…
cristianrcv Oct 2, 2026
c95bb45
feat(lineage): resolve runs by session ID and run name, and show the …
cristianrcv Oct 2, 2026
5c351a5
docs: document the commands and options added for API 1.230.0 [COMP-2…
cristianrcv Oct 2, 2026
07ea5c8
build(native): register reflection metadata for the API 1.230 command…
cristianrcv Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,16 @@ $ tw data-links browse -w seqeralabs/showcase -i v1-user-6d8f44c239e2a098b3e02e9

These commands act on an organization (`-o`/`--organization`). Most require the organization `OWNER` role.

### Service accounts

Run `tw service-accounts -h` to view the supported operations: `list`, `add`, `view`, `update`, and `delete`. Service accounts are non-human organization members. Add one to a workspace with `tw participants add -t MEMBER -n <name>`.

```bash
tw service-accounts add -o my-org -n ci-bot -d "CI pipeline launcher"
tw service-accounts list -o my-org
tw service-accounts list -w my-org/my-workspace
```

### Custom roles

Run `tw roles -h` to view the supported operations: `list`, `view`, `add`, `update`, `delete`, and `permissions`. `tw participants update --role` accepts custom role names.
Expand Down Expand Up @@ -792,13 +802,14 @@ tw audit-logs export --after 2026-09-01T00:00:00Z --before 2026-09-30T23:59:59Z
- AWS Cloud: `--log-group`. Google Cloud: `--project-id`.
- HPC platforms: `--propagate-head-job-options`.
- Seqera Compute: `--data-retention-policy`.
- `tw launch`, `tw pipelines add|update`, `tw runs relaunch`, and `tw actions add|update`: `--tower-config`. List the Nextflow versions you can launch with `tw info versions`.
- `tw launch`, `tw pipelines add|update`, `tw runs relaunch`, and `tw actions add|update`: `--fusion-version` and `--tower-config`. List the versions you can launch with `tw info versions`.
- `tw runs star|unstar`, `tw runs view log [--next <cursor>]`, and `tw runs delete -i <id>,<id>`.
- `tw pipelines schema`: show a pipeline's parameter schema.
- `tw actions add cron|bucket`.
- `tw actions add cron|bucket|pipeline-status` and `tw actions triggers list|view`.
- `tw studios star|unstar|extend|logs|rename-checkpoint`. `--mount-data-uris` also accepts a folder inside a data link.
- `tw data-links delete-content|download-script`.
- `tw datasets add --url`, `tw datasets update --url`, and `tw datasets disable-version`.
- `tw credentials federation-setup -p aws|google`: show the values to configure in your cloud provider before adding workload identity credentials.

[compute-envs]: https://docs.seqera.io/platform/latest/compute-envs/overview
[credentials]: https://docs.seqera.io/platform/latest/credentials/overview
Expand Down
15 changes: 13 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,13 @@ license {
exclude('conf/**')
}

// Jersey's HttpURLConnection connector can only send PATCH by reflecting into these JDK internals
// (HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, enabled by the SDK's ApiClient)
def jerseyPatchOpens = ['java.base/java.net', 'java.base/sun.net.www.protocol.https']

application {
mainClass.set('io.seqera.tower.cli.Tower')
applicationDefaultJvmArgs = jerseyPatchOpens.collect { "--add-opens=${it}=ALL-UNNAMED" }
}

java {
Expand All @@ -118,6 +123,8 @@ graalvmNative {
buildArgs.add('-H:+AddAllCharsets')
buildArgs.add('-H:EnableURLProtocols=https,http')
buildArgs.add('-H:+ReportExceptionStackTraces')
// Native images enforce module encapsulation for setAccessible too, so the reflection entries alone are not enough
buildArgs.addAll(jerseyPatchOpens.collect { "--add-opens=${it}=ALL-UNNAMED" })

javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(25)
Expand Down Expand Up @@ -168,7 +175,7 @@ tasks.named('run') {

// Run with GraalVM Tracing Agent enabled
if (System.getProperty('tracing-agent')?.toBoolean()) {
jvmArgs = ["-agentlib:native-image-agent=access-filter-file=conf/access-filter-file.json,config-merge-dir=conf/"]
jvmArgs += ["-agentlib:native-image-agent=access-filter-file=conf/access-filter-file.json,config-merge-dir=conf/"]
}
}

Expand All @@ -181,6 +188,9 @@ tasks.register('runReflectionConfigGenerator', JavaExec) {
}

tasks.named('shadowJar') {
manifest {
attributes 'Add-Opens': jerseyPatchOpens.join(' ')
}
archiveBaseName.set('tw')
archiveClassifier.set('')
archiveVersion.set('')
Expand All @@ -190,9 +200,10 @@ tasks.named('test') {
// Use junit platform for unit tests
useJUnitPlatform()
dependsOn tasks.named('checkLicenses')
jvmArgs jerseyPatchOpens.collect { "--add-opens=${it}=ALL-UNNAMED" }
// Run tests with GraalVM Tracing Agent enabled
if (System.getProperty('tracing-agent')?.toBoolean()) {
jvmArgs = ["-agentlib:native-image-agent=access-filter-file=conf/access-filter-file.json,config-merge-dir=conf/"]
jvmArgs += ["-agentlib:native-image-agent=access-filter-file=conf/access-filter-file.json,config-merge-dir=conf/"]
}
}

Expand Down
999 changes: 734 additions & 265 deletions conf/reflect-config.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mockserverVersion = "5.15.0"
picocliVersion = "4.6.3"
shadowVersion = "9.4.1"
slf4jVersion = "2.0.17"
towerJavaSdkVersion = "1.200.0"
towerJavaSdkVersion = "1.230.0"
xzVersion = "1.10"

[libraries]
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/seqera/tower/cli/Tower.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import io.seqera.tower.cli.commands.RolesCmd;
import io.seqera.tower.cli.commands.RunsCmd;
import io.seqera.tower.cli.commands.SecretsCmd;
import io.seqera.tower.cli.commands.ServiceAccountsCmd;
import io.seqera.tower.cli.commands.SshKeysCmd;
import io.seqera.tower.cli.commands.TeamsCmd;
import io.seqera.tower.cli.commands.WorkspacesCmd;
Expand Down Expand Up @@ -90,6 +91,7 @@
TeamsCmd.class,
WorkspacesCmd.class,
SecretsCmd.class,
ServiceAccountsCmd.class,
}
)
public class Tower extends AbstractCmd {
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.seqera.tower.api.AdminApi;
import io.seqera.tower.api.AgentsApi;
import io.seqera.tower.api.AvatarsApi;
import io.seqera.tower.api.CompatibilityApi;
import io.seqera.tower.api.ComputeEnvsApi;
import io.seqera.tower.api.CredentialsApi;
import io.seqera.tower.api.DataLinksApi;
Expand All @@ -39,6 +40,7 @@
import io.seqera.tower.api.PipelinesApi;
import io.seqera.tower.api.PlatformsApi;
import io.seqera.tower.api.RolesApi;
import io.seqera.tower.api.ServiceAccountsApi;
import io.seqera.tower.api.ServiceInfoApi;
import io.seqera.tower.api.SshKeysApi;
import io.seqera.tower.api.StudiosApi;
Expand All @@ -58,6 +60,7 @@
import io.seqera.tower.cli.exceptions.NoComputeEnvironmentException;
import io.seqera.tower.cli.exceptions.OrganizationNotFoundException;
import io.seqera.tower.cli.exceptions.ShowUsageException;
import io.seqera.tower.cli.exceptions.StatusCheckFailedException;
import io.seqera.tower.cli.exceptions.TowerException;
import io.seqera.tower.cli.exceptions.WorkspaceNotFoundException;
import io.seqera.tower.cli.responses.Response;
Expand Down Expand Up @@ -125,6 +128,7 @@ public abstract class AbstractApiCmd extends AbstractCmd {

private ActionsApi actionsApi;
private AvatarsApi avatarsApi;
private CompatibilityApi compatibilityApi;
private ComputeEnvsApi computeEnvsApi;
private CredentialsApi credentialsApi;
private DataLinksApi dataLinksApi;
Expand All @@ -141,6 +145,7 @@ public abstract class AbstractApiCmd extends AbstractCmd {
private PipelineVersionsApi pipelineVersionsApi;
private PlatformsApi platformsApi;
private RolesApi rolesApi;
private ServiceAccountsApi serviceAccountsApi;
private ServiceInfoApi serviceInfoApi;
private SshKeysApi sshKeysApi;
private StudiosApi studiosApi;
Expand Down Expand Up @@ -224,6 +229,10 @@ protected AvatarsApi avatarsApi() throws ApiException {
return avatarsApi == null ? new AvatarsApi(apiClient()) : avatarsApi;
}

protected CompatibilityApi compatibilityApi() throws ApiException {
return compatibilityApi == null ? new CompatibilityApi(apiClient()) : compatibilityApi;
}

protected ComputeEnvsApi computeEnvsApi() throws ApiException {
return computeEnvsApi == null ? new ComputeEnvsApi(apiClient()) : computeEnvsApi;
}
Expand Down Expand Up @@ -292,6 +301,10 @@ protected RolesApi rolesApi() throws ApiException {
return rolesApi == null ? new RolesApi(apiClient()) : rolesApi;
}

protected ServiceAccountsApi serviceAccountsApi() throws ApiException {
return serviceAccountsApi == null ? new ServiceAccountsApi(apiClient()) : serviceAccountsApi;
}

protected ServiceInfoApi serviceInfoApi() throws ApiException {
return serviceInfoApi == null ? new ServiceInfoApi(apiClient()) : serviceInfoApi;
}
Expand Down Expand Up @@ -699,8 +712,8 @@ public Integer call() {
return onBeforeExit(exitCode, response);
} catch (Exception e) {
errorMessage(app(), e);
return e instanceof StatusCheckFailedException ? StatusCheckFailedException.EXIT_CODE : CommandLine.ExitCode.SOFTWARE;
}
return CommandLine.ExitCode.SOFTWARE;
}

protected Integer onBeforeExit(int exitCode, Response response) throws ApiException {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/seqera/tower/cli/commands/ActionsCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.seqera.tower.cli.commands.actions.ListCmd;
import io.seqera.tower.cli.commands.actions.UpdateCmd;
import io.seqera.tower.cli.commands.actions.ViewCmd;
import io.seqera.tower.cli.commands.actions.triggers.TriggersCmd;
import picocli.CommandLine;

@CommandLine.Command(
Expand All @@ -34,6 +35,7 @@
AddCmd.class,
UpdateCmd.class,
LabelsCmd.class,
TriggersCmd.class,
}
)
public class ActionsCmd extends AbstractRootCmd {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.seqera.tower.cli.commands.credentials.AddCmd;
import io.seqera.tower.cli.commands.credentials.DeleteCmd;
import io.seqera.tower.cli.commands.credentials.FederationSetupCmd;
import io.seqera.tower.cli.commands.credentials.ListCmd;
import io.seqera.tower.cli.commands.credentials.UpdateCmd;
import io.seqera.tower.cli.commands.credentials.ValidateCmd;
Expand All @@ -32,7 +33,8 @@
UpdateCmd.class,
DeleteCmd.class,
ListCmd.class,
ValidateCmd.class
ValidateCmd.class,
FederationSetupCmd.class
}
)
public class CredentialsCmd extends AbstractRootCmd {
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/io/seqera/tower/cli/commands/LaunchCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.seqera.tower.cli.commands.pipelines.NullableLaunchOptions;
import io.seqera.tower.cli.commands.pipelines.versions.VersionRefOptions;
import io.seqera.tower.cli.exceptions.InvalidResponseException;
import io.seqera.tower.cli.exceptions.StatusCheckFailedException;
import io.seqera.tower.cli.responses.Response;
import io.seqera.tower.cli.responses.runs.RunSubmited;
import io.seqera.tower.model.ComputeEnvResponseDto;
Expand Down Expand Up @@ -89,7 +90,7 @@ public class LaunchCmd extends AbstractRootCmd {
@ArgGroup(multiplicity = "0..1")
VersionRefOptions.VersionRef versionRef;

@Option(names = {"--wait"}, description = "Wait until workflow reaches specified status: ${COMPLETION-CANDIDATES}")
@Option(names = {"--wait"}, description = "Wait until workflow reaches specified status: ${COMPLETION-CANDIDATES}. Exits with code 3 if the run was submitted but its status could not be checked.")
public WorkflowStatus wait;

@Option(names = {"-l", "--labels"}, split = ",", description = "Labels to assign to each pipeline run. Provide comma-separated label values (use key=value format for resource labels). Labels will be created if they don't exist", converter = Label.LabelConverter.class)
Expand Down Expand Up @@ -250,18 +251,22 @@ protected Integer onBeforeExit(int exitCode, Response response) {
() -> checkWorkflowStatus(submitted.workflowId, submitted.workspaceId),
WorkflowStatus.CANCELLED, WorkflowStatus.FAILED, WorkflowStatus.SUCCEEDED
);
} catch (StatusCheckFailedException e) {
String workspaceOption = submitted.workspaceId == null ? "" : " -w " + submitted.workspaceId;
throw new StatusCheckFailedException(String.format("Run %s was submitted but its status could not be checked (%s). It may still be running, check it with: tw runs view -i %s%s", submitted.workflowId, e.getReason(), submitted.workflowId, workspaceOption), e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return exitCode;
}
}

private WorkflowStatus checkWorkflowStatus(String workflowId, Long workspaceId) {
try {
return workflowsApi().describeWorkflow(workflowId, workspaceId, NO_WORKFLOW_ATTRIBUTES).getWorkflow().getStatus();
} catch (ApiException | NullPointerException e) {
return null;
private WorkflowStatus checkWorkflowStatus(String workflowId, Long workspaceId) throws ApiException {
WorkflowStatus status = workflowsApi().describeWorkflow(workflowId, workspaceId, NO_WORKFLOW_ATTRIBUTES).getWorkflow().getStatus();
// Platform lost contact with the run, which may still be running: keep polling as for a failed check
if (status == WorkflowStatus.UNKNOWN) {
throw new IllegalStateException("run status is UNKNOWN");
}
return status;
}

private List<Long> obtainLabelIDs(@Nullable Long workspaceId) throws ApiException {
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/io/seqera/tower/cli/commands/ServiceAccountsCmd.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2021-2026, Seqera.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.seqera.tower.cli.commands;

import io.seqera.tower.cli.commands.serviceaccounts.AddCmd;
import io.seqera.tower.cli.commands.serviceaccounts.DeleteCmd;
import io.seqera.tower.cli.commands.serviceaccounts.ListCmd;
import io.seqera.tower.cli.commands.serviceaccounts.UpdateCmd;
import io.seqera.tower.cli.commands.serviceaccounts.ViewCmd;
import picocli.CommandLine.Command;

@Command(
name = "service-accounts",
description = "Manage organization service accounts",
subcommands = {
ListCmd.class,
AddCmd.class,
ViewCmd.class,
UpdateCmd.class,
DeleteCmd.class,
}
)
public class ServiceAccountsCmd extends AbstractRootCmd {
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,31 @@
import io.seqera.tower.ApiException;
import io.seqera.tower.cli.commands.AbstractApiCmd;
import io.seqera.tower.cli.exceptions.ActionNotFoundException;
import io.seqera.tower.cli.exceptions.TowerException;
import io.seqera.tower.model.ActionQueryAttribute;
import io.seqera.tower.model.DescribeActionResponse;
import io.seqera.tower.model.ListActionsResponse;
import io.seqera.tower.model.ListActionsResponseActionInfo;
import io.seqera.tower.model.WorkflowStatus;
import picocli.CommandLine;

import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@CommandLine.Command
public abstract class AbstractActionsCmd extends AbstractApiCmd {

// The run states a pipeline-status action can fire on: the ones a run stops in.
private static final EnumSet<WorkflowStatus> TRIGGER_RUN_STATUSES = EnumSet.of(WorkflowStatus.SUCCEEDED, WorkflowStatus.FAILED, WorkflowStatus.CANCELLED);

protected static void checkTriggerRunStatus(WorkflowStatus runStatus) throws TowerException {
if (runStatus != null && !TRIGGER_RUN_STATUSES.contains(runStatus)) {
throw new TowerException(String.format("Run status '%s' cannot trigger an action. Use SUCCEEDED, FAILED or CANCELLED", runStatus));
}
}

protected ListActionsResponseActionInfo actionByName(Long workspaceId, String actionName) throws ApiException {
ListActionsResponse listActionResponse = actionsApi().listActions(workspaceId, NO_ACTION_ATTRIBUTES);

Expand All @@ -51,6 +63,12 @@ protected ListActionsResponseActionInfo actionByName(Long workspaceId, String ac
return listActionsResponseActionInfos.stream().findFirst().orElse(null);
}

protected String actionId(ActionRefOptions actionRefOptions, Long wspId) throws ApiException {
return actionRefOptions.action.actionId != null
? actionRefOptions.action.actionId
: actionByName(wspId, actionRefOptions.action.actionName).getId();
}

protected DescribeActionResponse fetchDescribeActionResponse(ActionRefOptions actionRefOptions, Long wspId, ActionQueryAttribute... attributes) throws ApiException {
DescribeActionResponse response;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.seqera.tower.cli.commands.actions.add.AddBucketCmd;
import io.seqera.tower.cli.commands.actions.add.AddCronCmd;
import io.seqera.tower.cli.commands.actions.add.AddGitHubCmd;
import io.seqera.tower.cli.commands.actions.add.AddPipelineStatusCmd;
import io.seqera.tower.cli.commands.actions.add.AddTowerCmd;
import io.seqera.tower.cli.exceptions.ShowUsageException;
import io.seqera.tower.cli.responses.Response;
Expand All @@ -35,6 +36,7 @@
AddTowerCmd.class,
AddCronCmd.class,
AddBucketCmd.class,
AddPipelineStatusCmd.class,
}
)
public class AddCmd extends AbstractActionsCmd {
Expand Down
Loading
Loading