From 01449439ae31a4218e718590bada042ce17239fb Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Fri, 19 Jun 2026 09:52:16 -0700 Subject: [PATCH 1/4] cleanup cli options --- .../dev/dbos/transact/cli/DBOSCommand.java | 9 +- .../dbos/transact/cli/DatabaseOptions.java | 6 - .../dev/dbos/transact/cli/MigrateCommand.java | 15 +- .../dbos/transact/cli/PostgresCommand.java | 229 ----------- .../dbos/transact/cli/WorkflowCommand.java | 356 ------------------ 5 files changed, 15 insertions(+), 600 deletions(-) delete mode 100644 transact-cli/src/main/java/dev/dbos/transact/cli/PostgresCommand.java delete mode 100644 transact-cli/src/main/java/dev/dbos/transact/cli/WorkflowCommand.java diff --git a/transact-cli/src/main/java/dev/dbos/transact/cli/DBOSCommand.java b/transact-cli/src/main/java/dev/dbos/transact/cli/DBOSCommand.java index db169c681..5cf806e91 100644 --- a/transact-cli/src/main/java/dev/dbos/transact/cli/DBOSCommand.java +++ b/transact-cli/src/main/java/dev/dbos/transact/cli/DBOSCommand.java @@ -8,14 +8,9 @@ @Command( name = "dbos", - description = "DBOS CLI is a command-line interface for managing DBOS workflows", + description = "DBOS CLI is a command-line interface for managing the DBOS system database", mixinStandardHelpOptions = true, - subcommands = { - MigrateCommand.class, - PostgresCommand.class, - ResetCommand.class, - WorkflowCommand.class - }, + subcommands = {MigrateCommand.class, ResetCommand.class}, versionProvider = DBOSCommand.class) public class DBOSCommand implements Runnable, IVersionProvider { diff --git a/transact-cli/src/main/java/dev/dbos/transact/cli/DatabaseOptions.java b/transact-cli/src/main/java/dev/dbos/transact/cli/DatabaseOptions.java index 921bb047e..62c23b11c 100644 --- a/transact-cli/src/main/java/dev/dbos/transact/cli/DatabaseOptions.java +++ b/transact-cli/src/main/java/dev/dbos/transact/cli/DatabaseOptions.java @@ -1,7 +1,5 @@ package dev.dbos.transact.cli; -import dev.dbos.transact.DBOSClient; - import picocli.CommandLine.Option; public class DatabaseOptions { @@ -45,8 +43,4 @@ public String password() { public String schema() { return this.schema; } - - public DBOSClient createClient() { - return new DBOSClient(url(), user(), password(), schema()); - } } diff --git a/transact-cli/src/main/java/dev/dbos/transact/cli/MigrateCommand.java b/transact-cli/src/main/java/dev/dbos/transact/cli/MigrateCommand.java index 6a00da13d..631c2eef4 100644 --- a/transact-cli/src/main/java/dev/dbos/transact/cli/MigrateCommand.java +++ b/transact-cli/src/main/java/dev/dbos/transact/cli/MigrateCommand.java @@ -21,6 +21,14 @@ public class MigrateCommand implements Callable { description = "The role with which you will run your DBOS application") String appRole; + @Option( + names = {"--listen-notify"}, + negatable = true, + defaultValue = "true", + description = + "Use LISTEN/NOTIFY on the DBOS system database [default: ${DEFAULT-VALUE}]. Use --no-listen-notify to disable.") + boolean useListenNotify; + @Mixin DatabaseOptions dbOptions; @Option( @@ -38,9 +46,12 @@ public Integer call() throws Exception { out.format(" System Database: %s\n", dbOptions.url()); out.format(" System Database User: %s\n", dbOptions.user()); - // TODO: add option for useListenNotify MigrationManager.runMigrations( - dbOptions.url(), dbOptions.user(), dbOptions.password(), dbOptions.schema(), true); + dbOptions.url(), + dbOptions.user(), + dbOptions.password(), + dbOptions.schema(), + useListenNotify); grantDBOSSchemaPermissions(out, dbOptions.schema()); return 0; } diff --git a/transact-cli/src/main/java/dev/dbos/transact/cli/PostgresCommand.java b/transact-cli/src/main/java/dev/dbos/transact/cli/PostgresCommand.java deleted file mode 100644 index bbcf9e2c3..000000000 --- a/transact-cli/src/main/java/dev/dbos/transact/cli/PostgresCommand.java +++ /dev/null @@ -1,229 +0,0 @@ -package dev.dbos.transact.cli; - -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Objects; -import java.util.concurrent.Callable; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Option; -import picocli.CommandLine.Spec; -import tools.jackson.databind.json.JsonMapper; - -@Command( - name = "postgres", - aliases = {"pg"}, - description = "Manage local Postgres database with Docker", - subcommands = {StartCommand.class, StopCommand.class}) -public class PostgresCommand implements Runnable { - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Override - public void run() { - CommandLine cmd = new CommandLine(this); - cmd.usage(System.out); - } - - public static String inspectContainerStatus(String containerName) throws Exception { - var result = CommandResult.execute("docker", "inspect", containerName); - if (result.exitCode() == 0) { - var mapper = new JsonMapper(); - var root = mapper.readTree(result.stdout()); - return root.get(0).get("State").get("Status").stringValue(); - } else { - return null; - } - } -} - -@Command(name = "start", description = "Start a local Postgres database Docker container") -class StartCommand implements Callable { - - @Option( - names = {"-c", "--container-name"}, - description = "Container name [default: ${DEFAULT-VALUE}]") - String containerName = "dbos-db"; - - @Option( - names = {"-i", "--image-name"}, - description = "Image name [default: ${DEFAULT-VALUE}]") - String imageName = "pgvector/pgvector:pg16"; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public Integer call() throws Exception { - var out = spec.commandLine().getOut(); - - if (!checkDockerInstalled()) { - out.println("Docker not installed locally"); - return 1; - } - - var port = 5432; - var password = Objects.requireNonNullElse(System.getenv("PGPASSWORD"), "dbos"); - startDockerPostgres(out, containerName, imageName, password, port); - - out.format( - "Postgres available at jdbc:postgresql://localhost:%d/postgres with user=postgres and password=%s\n", - port, password); - // postgresql://postgres:%s@localhost:%d\n", password, port); - return 0; - } - - static boolean checkDockerInstalled() throws Exception { - var result = CommandResult.execute("docker", "version", "--format", "json"); - return result.exitCode() == 0; - } - - static void startDockerPostgres( - PrintWriter out, String containerName, String imageName, String password, int port) - throws Exception { - var pgData = "/var/lib/postgresql/data"; - - out.println("Starting a Postgres Docker container..."); - - try { - var status = PostgresCommand.inspectContainerStatus(containerName); - if (status != null && status.equals("running")) { - out.format("Container %s is already running\n", containerName); - return; - } - if (status != null && status.equals("exited")) { - CommandResult.checkExecute("docker", "start", containerName); - out.format("Container %s was stopped and has been restarted\n", containerName); - return; - } - } catch (Exception e) { - // ignore exception, proceed with creation - } - - var queryImagesResult = CommandResult.execute("docker", "images", "-q", imageName); - if (queryImagesResult.stdout().trim().isEmpty()) { - out.format("Pulling docker image %s\n", imageName); - CommandResult.checkExecute("docker", "pull", imageName); - } - - var runResult = - CommandResult.checkExecute( - "docker", - "run", - "-d", - "--name", - containerName, - "-e", - "POSTGRES_PASSWORD=%s".formatted(password), - "-e", - "PGDATA=%s".formatted(pgData), - "-p", - "%d:5432".formatted(port), - "-v", - "%1$s:%1$s".formatted(pgData), - "--rm", - imageName); - - out.format("created container %s\n", runResult.trim()); - - var url = "jdbc:postgresql://localhost:%d/postgres".formatted(port); - var user = "postgres"; - for (var i = 0; i < 30; i++) { - if (i % 5 == 0) { - out.println("Waiting for Postgres Docker container to start..."); - } - var result = checkConnectivity(url, user, password); - if (result == null) { - return; - } - Thread.sleep(1000); - } - - var msg = - "Failed to start Docker container: Container %s did not start in time." - .formatted(containerName); - throw new RuntimeException(msg); - } - - static SQLException checkConnectivity(String url, String user, String password) { - try (var conn = DriverManager.getConnection(url, user, password); - var stmt = conn.createStatement()) { - stmt.execute("SELECT 1"); - return null; - } catch (SQLException e) { - return e; - } - } -} - -@Command(name = "stop", description = "Stop the local Postgres database Docker container") -class StopCommand implements Callable { - - @Option( - names = {"-c", "--container-name"}, - description = "Container name [default: ${DEFAULT-VALUE}]") - String containerName = "dbos-db"; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public Integer call() throws Exception { - var out = spec.commandLine().getOut(); - - out.format("Stopping Docker Postgres container %s\n", containerName); - var status = PostgresCommand.inspectContainerStatus(containerName); - if (status == null) { - out.format("Container %s does not exist\n", containerName); - } else if (status.equals("running")) { - CommandResult.checkExecute("docker", "stop", containerName); - out.format("Successfully stopped Docker Postgres container %s\n", containerName); - } else { - out.format("Container %s exists but is not running\n", containerName); - } - - return 0; - } -} - -record CommandResult(int exitCode, String stdout, String stderr) { - public static CommandResult execute(String... command) throws IOException, InterruptedException { - var process = new ProcessBuilder(command).start(); - int exitCode = process.waitFor(); - - var stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - var stderr = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); - - return new CommandResult(exitCode, stdout, stderr); - } - - public static String checkExecute(String... command) throws IOException, InterruptedException { - var process = new ProcessBuilder(command).start(); - int exitCode = process.waitFor(); - if (exitCode != 0) { - var stderr = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); - throw new RuntimeException(stderr); - } - var stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - return stdout; - } -} diff --git a/transact-cli/src/main/java/dev/dbos/transact/cli/WorkflowCommand.java b/transact-cli/src/main/java/dev/dbos/transact/cli/WorkflowCommand.java deleted file mode 100644 index 0195a5831..000000000 --- a/transact-cli/src/main/java/dev/dbos/transact/cli/WorkflowCommand.java +++ /dev/null @@ -1,356 +0,0 @@ -package dev.dbos.transact.cli; - -import dev.dbos.transact.workflow.ForkOptions; -import dev.dbos.transact.workflow.ListWorkflowsInput; -import dev.dbos.transact.workflow.WorkflowState; - -import java.time.Instant; -import java.util.List; -import java.util.Objects; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; -import picocli.CommandLine.Spec; -import tools.jackson.databind.json.JsonMapper; - -@Command( - name = "workflow", - aliases = {"wf"}, - description = "Manage DBOS workflows", - subcommands = { - ListCommand.class, - GetCommand.class, - StepsCommand.class, - CancelCommand.class, - ResumeCommand.class, - ForkCommand.class, - }) -public class WorkflowCommand implements Runnable { - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Override - public void run() { - CommandLine cmd = new CommandLine(this); - cmd.usage(System.out); - } - - public static String prettyPrint(Object object) { - var mapper = new JsonMapper(); - var writer = mapper.writerWithDefaultPrettyPrinter(); - return writer.writeValueAsString(Objects.requireNonNull(object)); - } -} - -@Command(name = "list", description = "List workflows for your application") -class ListCommand implements Runnable { - - @Option( - names = {"-s", "--start-time"}, - description = "Retrieve workflows starting after this timestamp (ISO 8601 format)") - String startTime; - - @Option( - names = {"-e", "--end-time"}, - description = "Retrieve workflows starting before this timestamp (ISO 8601 format)") - String endTime; - - @Option( - names = {"-S", "--status"}, - description = - "Retrieve workflows with this status (PENDING, SUCCESS, ERROR, ENQUEUED, CANCELLED, or MAX_RECOVERY_ATTEMPTS_EXCEEDED)") - String status; - - @Option( - names = {"-n", "--name"}, - description = "Retrieve workflows with this name") - String workflowName; - - @Option( - names = {"-v", "--app-version"}, - description = "Retrieve workflows with this application version") - String appVersion; - - @Option( - names = {"-q", "--queue"}, - description = "Retrieve workflows on this queue") - String queue; - - // @Option( - // names = {"-u", "--user"}, - // description = "Retrieve workflows run by this user") - // String user; - - @Option( - names = {"-d", "--sort-desc"}, - description = "Sort the results in descending order (older first)") - boolean sortDescending; - - @Option( - names = {"-l", "--limit"}, - description = "Limit the results returned", - defaultValue = "10") - int limit; - - @Option( - names = {"-o", "--offset"}, - description = "Offset for pagination", - defaultValue = "0") - int offset; - - @Option( - names = {"-Q", "--queues-only"}, - description = "Retrieve only queued workflows") - boolean queuesOnly; - - @Mixin DatabaseOptions dbOptions; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public void run() { - var out = spec.commandLine().getOut(); - - var input = - new ListWorkflowsInput( - null, // workflowIds - status != null ? List.of(WorkflowState.valueOf(status)) : null, - startTime != null ? Instant.parse(startTime) : null, - endTime != null ? Instant.parse(endTime) : null, - workflowName != null ? List.of(workflowName) : null, - null, // className - null, // instanceName - appVersion != null ? List.of(appVersion) : null, - null, // authenticatedUser - limit, - offset, - sortDescending, - null, // workflowIdPrefix - false, // loadInput - false, // loadOutput - queue != null ? List.of(queue) : null, - queuesOnly, - null, // executorIds - null, // forkedFrom - null, // parentWorkflowId - null, // wasForkedFrom - null, // hasParent - null // attributes - ); - - var client = dbOptions.createClient(); - var workflows = client.listWorkflows(input); - var json = WorkflowCommand.prettyPrint(workflows); - out.println(json); - } -} - -@Command(name = "get", description = "Retrieve the status of a workflow") -class GetCommand implements Runnable { - - @Parameters(index = "0", description = "Workflow ID to retrieve") - String workflowId; - - @Mixin DatabaseOptions dbOptions; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public void run() { - var out = spec.commandLine().getOut(); - - Objects.requireNonNull(workflowId, "workflowId parameter cannot be null"); - var input = - new ListWorkflowsInput( - List.of(workflowId), // workflowIds - null, // status - null, // startTime - null, // endTime - null, // workflowName - null, // className - null, // instanceName - null, // applicationVersion - null, // authenticatedUser - null, // limit - null, // offset - null, // sortDesc - null, // workflowIdPrefix - false, // loadInput - false, // loadOutput - null, // queueName - false, // queuesOnly - null, // executorIds - null, // forkedFrom - null, // parentWorkflowId - null, // wasForkedFrom - null, // hasParent - null // attributes - ); - var client = dbOptions.createClient(); - var workflows = client.listWorkflows(input); - if (workflows.isEmpty()) { - System.err.println("Failed to retrieve workflow %s".formatted(workflowId)); - } else { - var json = WorkflowCommand.prettyPrint(workflows.get(0)); - out.println(json); - } - } -} - -@Command(name = "steps", description = "List the steps of a workflow") -class StepsCommand implements Runnable { - - @Parameters(index = "0", description = "Workflow ID to list steps for") - String workflowId; - - @Mixin DatabaseOptions dbOptions; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public void run() { - var out = spec.commandLine().getOut(); - - var client = dbOptions.createClient(); - var steps = - client.listWorkflowSteps( - Objects.requireNonNull(workflowId, "workflowId parameter cannot be null")); - var json = WorkflowCommand.prettyPrint(steps); - out.println(json); - } -} - -@Command( - name = "cancel", - description = "Cancel a workflow so it is no longer automatically retried or restarted") -class CancelCommand implements Runnable { - - @Parameters(index = "0", description = "Workflow ID to cancel") - String workflowId; - - @Mixin DatabaseOptions dbOptions; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public void run() { - var out = spec.commandLine().getOut(); - - var client = dbOptions.createClient(); - client.cancelWorkflow( - Objects.requireNonNull(workflowId, "workflowId parameter cannot be null")); - out.format("Successfully cancelled workflow %s\n", workflowId); - } -} - -@Command(name = "resume", description = "Resume a workflow that has been cancelled") -class ResumeCommand implements Runnable { - - @Parameters(index = "0", description = "Workflow ID to resume") - String workflowId; - - @Mixin DatabaseOptions dbOptions; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public void run() { - var out = spec.commandLine().getOut(); - - var client = dbOptions.createClient(); - var handle = - client.resumeWorkflow( - Objects.requireNonNull(workflowId, "workflowId parameter cannot be null")); - var json = WorkflowCommand.prettyPrint(handle.getStatus()); - out.println(json); - } -} - -@Command(name = "fork", description = "Fork a workflow from the beginning or from a specific step") -class ForkCommand implements Runnable { - - @Parameters(index = "0", description = "Workflow ID to fork") - String workflowId; - - @Option( - names = {"-f", "--forked-workflow-id"}, - description = "Custom workflow ID for the forked workflow") - String forkedWorkflowId; - - @Option( - names = {"-v", "--application-version"}, - description = "Application version for the forked workflow") - String appVersion; - - @Option( - names = {"-s", "--step"}, - description = "Restart from this step [default: ${DEFAULT-VALUE}]", - defaultValue = "1") - Integer step; - - @Mixin DatabaseOptions dbOptions; - - @Option( - names = {"-h", "--help"}, - usageHelp = true, - description = "Display this help message") - boolean help; - - @Spec CommandSpec spec; - - @Override - public void run() { - var out = spec.commandLine().getOut(); - - int step = this.step == null ? 1 : this.step; - var client = dbOptions.createClient(); - var options = new ForkOptions(); - if (forkedWorkflowId != null) { - options = options.withForkedWorkflowId(forkedWorkflowId); - } - if (appVersion != null) { - options = options.withApplicationVersion(appVersion); - } - var handle = client.forkWorkflow(workflowId, step, options); - var json = WorkflowCommand.prettyPrint(handle.getStatus()); - out.println(json); - } -} From e6b54e21aec5c982fec1d7f61268f48055b1c7bc Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Fri, 19 Jun 2026 10:41:01 -0700 Subject: [PATCH 2/4] build native images of cli (including gh ci changes) --- .github/workflows/publish.yml | 136 ++++++++++++-- .../main/kotlin/dev/dbos/build/GitVersion.kt | 2 +- gradle/libs.versions.toml | 3 + transact-cli/README.md | 166 +++++------------- transact-cli/build.gradle.kts | 27 ++- 5 files changed, 196 insertions(+), 138 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 85868e975..e87137546 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,13 +10,12 @@ on: SIGNING_KEY: required: true SIGNING_KEY_PASSWORD: - required: true + required: true workflow_dispatch: jobs: publish: runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/v') steps: - name: Checkout code uses: actions/checkout@v6 @@ -29,7 +28,12 @@ jobs: java-version: '21' distribution: 'temurin' - - name: Publish + - name: Build + run: ./gradlew assemble + + # only publish main & release/* builds to maven central + - name: Publish to Maven Central + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') run: ./gradlew publishToMavenCentral env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} @@ -37,33 +41,141 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + - name: Upload jars + uses: actions/upload-artifact@v4 + with: + name: jars + path: transact*/build/libs/*.jar + + # GraalVM Native Image cannot cross-compile, so each platform's binary is + # built on its own native runner. + # + # windows-arm64 is intentionally absent: GraalVM ships no windows-aarch64 + # Native Image distribution, so native-image cannot run there (even though + # GitHub now offers windows-11-arm runners). Windows-on-ARM runs the + # windows-x64 binary via the OS's built-in x64 emulation. + # + # Native images are built on every run and uploaded as build artifacts; the + # `release` job only attaches them to a GitHub Release for release builds. + build-native: + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, platform: linux-x64, ext: '' } + - { os: ubuntu-24.04-arm, platform: linux-arm64, ext: '' } + - { os: macos-latest, platform: macos-arm64, ext: '' } + - { os: macos-13, platform: macos-x64, ext: '' } + - { os: windows-latest, platform: windows-x64, ext: '.exe' } + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 # fetch-depth 0 needed for version calculation + + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + java-version: '21' + distribution: 'graalvm-community' + + - name: Build native image + # auto-download=false forces Gradle to use the GraalVM installed above + # rather than re-provisioning a toolchain via the foojay resolver. + run: ./gradlew :transact-cli:nativeCompile -Porg.gradle.java.installations.auto-download=false + shell: bash + + - name: Package binary + shell: bash + run: | + mkdir -p staged + cd transact-cli/build/native/nativeCompile + if [ -n "${{ matrix.ext }}" ]; then + 7z a "$GITHUB_WORKSPACE/staged/dbos-${{ matrix.platform }}.zip" "dbos${{ matrix.ext }}" + else + tar czf "$GITHUB_WORKSPACE/staged/dbos-${{ matrix.platform }}.tar.gz" dbos + fi + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: dbos-${{ matrix.platform }} + path: staged/* + + release: + needs: [publish, build-native] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/release/') + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 # fetch-depth 0 needed for version calculation + + - name: Set up OpenJDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + - name: Get Gradle project version id: get_version run: | VERSION=$(./gradlew properties -q | grep '^version:' | awk '{print $2}') - COMMIT_COUNT=$(./gradlew properties -q | grep '^commitCount:' | awk '{print $2}') TAGS=$(git tag --points-at HEAD) - echo "Project version: $VERSION, commit Count: $COMMIT_COUNT, tags: $TAGS" + echo "Project version: $VERSION, tags: $TAGS" if [ -z "$TAGS" ]; then echo "is_tagged=false" >> $GITHUB_OUTPUT else echo "is_tagged=true" >> $GITHUB_OUTPUT fi + + # A final release is bare semver (X.Y.Z); any suffix (-rc, -m, -a) is a prerelease. + if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "prerelease=false" >> $GITHUB_OUTPUT + else + echo "prerelease=true" >> $GITHUB_OUTPUT + fi echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "commit_count=$COMMIT_COUNT" >> $GITHUB_OUTPUT + + - name: Download jars + uses: actions/download-artifact@v4 + with: + name: jars + path: jars + + - name: Download native binaries + uses: actions/download-artifact@v4 + with: + path: native + pattern: dbos-* + merge-multiple: true + + - name: Stage release assets + id: stage + run: | + VERSION="${{ steps.get_version.outputs.version }}" + mkdir -p release-assets + # Include the fat CLI jar (dbos.jar) and every other module's jars, + # but exclude the thin transact-cli library jar (transact-cli-*.jar). + find jars -name '*.jar' ! -name 'transact-cli-*.jar' -exec cp {} release-assets/ \; + for f in native/dbos-*; do + base=$(basename "$f") # e.g. dbos-linux-x64.tar.gz + rest=${base#dbos-} # e.g. linux-x64.tar.gz + cp "$f" "release-assets/dbos-${VERSION}-${rest}" + done + ls -la release-assets - name: Github Release - if: | - startsWith(github.ref, 'refs/heads/release/v') && - steps.get_version.outputs.is_tagged == 'true' + if: steps.get_version.outputs.is_tagged == 'true' uses: softprops/action-gh-release@v2.1.0 with: name: ${{ steps.get_version.outputs.version }} tag_name: ${{ steps.get_version.outputs.version }} target_commitish: ${{ github.sha }} - prerelease: ${{ steps.get_version.outputs.commit_count != '0' }} + prerelease: ${{ steps.get_version.outputs.prerelease }} files: | - transact*/build/libs/*.jar - + release-assets/* diff --git a/build-logic/src/main/kotlin/dev/dbos/build/GitVersion.kt b/build-logic/src/main/kotlin/dev/dbos/build/GitVersion.kt index 3d9755e90..79fe72be0 100644 --- a/build-logic/src/main/kotlin/dev/dbos/build/GitVersion.kt +++ b/build-logic/src/main/kotlin/dev/dbos/build/GitVersion.kt @@ -45,7 +45,7 @@ object GitVersion { return when { branch == "main" -> "$major.${minor + 1}.$patch-m$commitCount" - branch.startsWith("release/v") -> + branch.startsWith("release/") -> if (commitCount == 0) "$major.$minor.$patch" else "$major.$minor.${patch + 1}-rc$commitCount" else -> "$major.${minor + 1}.$patch-a$commitCount-g${gitHash(root)}" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1297c09c8..3f79d9a96 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ aspectj = "1.9.25.1" assertj = "3.27.7" cron-utils = "9.2.1" +graalvm-native = "0.11.1" hibernate = "7.4.1.Final" hikaricp = "7.1.0" jackson = "3.2.0" @@ -60,6 +61,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo maven-artifact = { module = "org.apache.maven:maven-artifact", version.ref = "maven-artifact" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } +picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } rest-assured = { module = "io.rest-assured:rest-assured", version.ref = "rest-assured" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } @@ -82,6 +84,7 @@ testcontainers-postgresql = { module = "org.testcontainers:testcontainers-postgr testcontainers-toxiproxy = { module = "org.testcontainers:testcontainers-toxiproxy", version.ref = "testcontainers" } [plugins] +graalvm-native = { id = "org.graalvm.buildtools.native", version.ref = "graalvm-native" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/transact-cli/README.md b/transact-cli/README.md index a162fa4eb..0f662248d 100644 --- a/transact-cli/README.md +++ b/transact-cli/README.md @@ -1,17 +1,44 @@ # DBOS CLI -The DBOS CLI is a command-line interface for managing DBOS workflows. +The DBOS CLI is a command-line interface for managing the DBOS system database. ## Installation -DBOS CLI is distributed as a JAR with dependencies (also known as a fat or uber JAR). -It is run from the command line with the `-jar` option of the [`java` command](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html). +DBOS CLI is distributed in two forms: + +* A native executable (`dbos`) that runs directly without a JVM. This is the + recommended way to run the CLI. + + ```shell + $ dbos --version + dbos v0.10.0 + ``` + +* A JAR with dependencies (also known as a fat or uber JAR), run with the + `-jar` option of the [`java` command](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html). + + ```shell + $ java -jar dbos.jar --version + dbos v0.10.0 + ``` + +The examples below use the native `dbos` executable; substitute +`java -jar dbos.jar` if you are using the JAR. + +### Building the native executable + +The native executable is built with [GraalVM Native Image](https://www.graalvm.org/latest/reference-manual/native-image/) +via the [Gradle native-build-tools plugin](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html): ```shell -$ java -jar dbos.jar --version -dbos v0.7.0 +./gradlew :transact-cli:nativeCompile ``` +The build downloads a GraalVM toolchain automatically (via the foojay toolchain +resolver) and writes the executable to +`transact-cli/build/native/nativeCompile/dbos`. Native Image requires a local C +toolchain (`gcc`/`clang`, `glibc`/`zlib` development headers) on the build machine. + ## Configuration ### Database Connection Configuration @@ -33,13 +60,13 @@ These values can be specified on the command line or via environment variables. Example: ```bash # Using command-line flag (highest priority) -java -jar dbos.jar migrate --db-url jdbc:postgresql://localhost/mydb -U user -P password +dbos migrate --db-url jdbc:postgresql://localhost/mydb -U user -P password # Using environment variable (lowest priority) export DBOS_SYSTEM_JDBC_URL=jdbc:postgresql://localhost/mydb export PGUSER=user export PGPASSWORD=password -java -jar dbos.jar migrate +dbos migrate ``` ### Database Schema Configuration @@ -49,10 +76,7 @@ By default, DBOS creates its system tables in the `dbos` schema. You can specify Example: ```bash # Use a custom schema for all DBOS system tables -java -jar dbos.jar migrate --schema myapp_schema - -# All workflow commands will use the specified schema -java -jar dbos.jar workflow list --schema myapp_schema +dbos migrate --schema myapp_schema ``` ## Commands @@ -62,11 +86,13 @@ Create DBOS system tables in your database. This command runs the migration comm **Options:** - `-r, --app-role ` - The role with which you will run your DBOS application +- `--[no-]listen-notify` - Use LISTEN/NOTIFY on the DBOS system database (default: enabled). Pass `--no-listen-notify` to disable. **Usage:** ```bash -java -jar dbos.jar migrate -java -jar dbos.jar migrate --app-role myapp_role +dbos migrate +dbos migrate --app-role myapp_role +dbos migrate --no-listen-notify ``` ### `dbos reset` @@ -77,116 +103,8 @@ Reset the DBOS system database, deleting metadata about past workflows and steps **Usage:** ```bash -java -jar dbos.jar reset -java -jar dbos.jar reset --yes # Skip confirmation -``` - -### `dbos postgres` -Manage a local PostgreSQL database with Docker for development. - -#### `dbos postgres start` -Start a local Postgres database container with pgvector extension. - -**Options:** -- `-c, --container-name` - Docker container name, defaults to dbos-db -- `-i, --image-name` - Docker image name, defaults to pgvector/pgvector:pg16 - -**Usage:** -```bash -java -jar dbos.jar postgres start -``` - -Creates a PostgreSQL container with: -- Container name: `dbos-db` (unless overridden by `--container-name`) -- Port: 5432 -- Default database: `dbos` -- Default user: `postgres` -- Default password: `dbos` - -#### `dbos postgres stop` -Stop the local Postgres database container. - -**Usage:** -```bash -java -jar dbos.jar postgres stop -``` -**Options:** -- `-c, --container-name` - Docker container name, defaults to dbos-db - -### `dbos workflow` -Manage DBOS workflows. - -#### `dbos workflow list` -List workflows for your application. - -**Options:** -- `-l, --limit ` - Limit the results returned (default: 10) -- `-o, --offset ` - Offset for pagination -- `-S, --status ` - Filter by status (PENDING, SUCCESS, ERROR, ENQUEUED, CANCELLED, or MAX_RECOVERY_ATTEMPTS_EXCEEDED) -- `-n, --name ` - Retrieve workflows with this name -- `-v, --application-version ` - Retrieve workflows with this application version -- `-s, --start-time ` - Retrieve workflows starting after this timestamp (ISO 8601) -- `-e, --end-time ` - Retrieve workflows starting before this timestamp (ISO 8601) -- `-q, --queue ` - Retrieve workflows on this queue -- `-Q, --queues-only` - Retrieve only queued workflows -- `-d, --sort-desc` - Sort the results in descending order (older first) - -**Usage:** -```bash -java -jar dbos.jar workflow list -java -jar dbos.jar workflow list --limit 50 --status SUCCESS -java -jar dbos.jar workflow list --name "ProcessOrder" -``` - -#### `dbos workflow get [workflow-id]` -Retrieve the status of a specific workflow. - -**Usage:** -```bash -java -jar dbos.jar workflow get abc123def456 -``` - -Returns detailed information about the workflow including its status, start time, end time, and other metadata. - -#### `dbos workflow steps [workflow-id]` -List the steps of a workflow. - -**Usage:** -```bash -java -jar dbos.jar workflow steps abc123def456 -``` - -Shows all the steps executed within a workflow, their status, and execution details. - -#### `dbos workflow cancel [workflow-id]` -Cancel a workflow so it is no longer automatically retried or restarted. - -**Usage:** -```bash -java -jar dbos.jar workflow cancel abc123def456 -``` - -#### `dbos workflow resume [workflow-id]` -Resume a workflow that has been cancelled. - -**Usage:** -```bash -java -jar dbos.jar workflow resume abc123def456 -``` - -#### `dbos workflow fork [workflow-id]` -Fork a workflow from the beginning or from a specific step. - -**Options:** -- `-s, --step ` - Restart from this step (default: 1) -- `-f, --forked-workflow-id ` - Custom workflow ID for the forked workflow -- `-a, --application-version ` - Application version for the forked workflow - -**Usage:** -```bash -java -jar dbos.jar workflow fork abc123def456 -java -jar dbos.jar workflow fork abc123def456 --step 3 -java -jar dbos.jar workflow fork abc123def456 --forked-workflow-id custom-id-123 +dbos reset +dbos reset --yes # Skip confirmation ``` ### `dbos version` @@ -194,7 +112,7 @@ Show the version and exit. **Usage:** ```bash -java -jar dbos.jar --version +dbos --version ``` ## License diff --git a/transact-cli/build.gradle.kts b/transact-cli/build.gradle.kts index e65dd33e4..42336b0cd 100644 --- a/transact-cli/build.gradle.kts +++ b/transact-cli/build.gradle.kts @@ -1,6 +1,9 @@ +import org.graalvm.buildtools.gradle.dsl.GraalVMExtension + plugins { application alias(libs.plugins.shadow) + alias(libs.plugins.graalvm.native) id("dbos.java-conventions") id("dbos.quality-conventions") } @@ -9,8 +12,8 @@ application { mainClass.set("dev.dbos.transact.cli.Main") } dependencies { implementation(project(":transact")) - implementation(libs.jackson.databind) implementation(libs.picocli) + annotationProcessor(libs.picocli.codegen) runtimeOnly(libs.slf4j.simple) testImplementation(platform(libs.junit.bom)) @@ -19,8 +22,30 @@ dependencies { testImplementation(libs.testcontainers.postgresql) } +tasks.named("compileJava") { + options.compilerArgs.add("-Aproject=${project.group}/${project.name}") +} + tasks.named("shadowJar") { archiveBaseName.set("dbos") archiveVersion.set("") archiveClassifier.set("") } + +configure { + metadataRepository { enabled.set(true) } + + binaries { + named("main") { + imageName.set("dbos") + mainClass.set("dev.dbos.transact.cli.Main") + buildArgs.add("--no-fallback") + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(21)) + vendor.set(JvmVendorSpec.GRAAL_VM) + } + ) + } + } +} From 360297254985b227d3568b7ac08ca073d9891468 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Fri, 19 Jun 2026 12:21:55 -0700 Subject: [PATCH 3/4] fix native build on windows & macos --- .github/workflows/publish.yml | 19 ++++++++++++++----- .github/workflows/test_demo_apps.yml | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e87137546..53fbe86e7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,7 +42,7 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} - name: Upload jars - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: jars path: transact*/build/libs/*.jar @@ -65,7 +65,7 @@ jobs: - { os: ubuntu-latest, platform: linux-x64, ext: '' } - { os: ubuntu-24.04-arm, platform: linux-arm64, ext: '' } - { os: macos-latest, platform: macos-arm64, ext: '' } - - { os: macos-13, platform: macos-x64, ext: '' } + - { os: macos-15-intel, platform: macos-x64, ext: '' } - { os: windows-latest, platform: windows-x64, ext: '.exe' } runs-on: ${{ matrix.os }} steps: @@ -85,6 +85,15 @@ jobs: # rather than re-provisioning a toolchain via the foojay resolver. run: ./gradlew :transact-cli:nativeCompile -Porg.gradle.java.installations.auto-download=false shell: bash + env: + # Windows-only: native-image puts its scratch dir under the system + # temp on C:, but the workspace is on D:. The build relativizes one + # against the other, which throws "'other' has different root" since + # NIO can't relativize across drives. Pinning temp to RUNNER_TEMP + # (also on D:) keeps both paths on the same drive. No-op elsewhere. + # See https://github.com/oracle/graal/issues/11795 + TMP: ${{ runner.temp }} + TEMP: ${{ runner.temp }} - name: Package binary shell: bash @@ -98,7 +107,7 @@ jobs: fi - name: Upload binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dbos-${{ matrix.platform }} path: staged/* @@ -142,13 +151,13 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Download jars - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: jars path: jars - name: Download native binaries - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: native pattern: dbos-* diff --git a/.github/workflows/test_demo_apps.yml b/.github/workflows/test_demo_apps.yml index 0da533b29..3b78ab1fb 100644 --- a/.github/workflows/test_demo_apps.yml +++ b/.github/workflows/test_demo_apps.yml @@ -90,7 +90,7 @@ jobs: path: dbos-demo-apps - name: Download Maven Local artifacts - uses: actions/download-artifact@v7 + uses: actions/actions/download-artifact@v8 with: name: dbos-maven-local path: ~/.m2/repository/dev/dbos From 69dc7e5e9b5598d316065200b0ff1be65563fa76 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Fri, 19 Jun 2026 12:35:31 -0700 Subject: [PATCH 4/4] download-artifact@v7 --- .github/workflows/publish.yml | 4 ++-- .github/workflows/test_demo_apps.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 53fbe86e7..d6270dc14 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -151,13 +151,13 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Download jars - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: jars path: jars - name: Download native binaries - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: path: native pattern: dbos-* diff --git a/.github/workflows/test_demo_apps.yml b/.github/workflows/test_demo_apps.yml index 3b78ab1fb..0da533b29 100644 --- a/.github/workflows/test_demo_apps.yml +++ b/.github/workflows/test_demo_apps.yml @@ -90,7 +90,7 @@ jobs: path: dbos-demo-apps - name: Download Maven Local artifacts - uses: actions/actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: dbos-maven-local path: ~/.m2/repository/dev/dbos