From 2c0edac4a9c47b4db9494d2fef57a8d419b148b7 Mon Sep 17 00:00:00 2001 From: Kyle S Passarelli Date: Mon, 19 Oct 2026 09:13:29 -0600 Subject: [PATCH 1/2] Change target syntax to align with LINK --- README.md | 17 ++++++++++------- devops-test.el | 25 +++++++++++++++++++++++++ devops.el | 18 ++++++++++-------- examples/1_commands.org | 8 ++++---- examples/2_tangle.org | 4 ++-- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b71856c..592580b 100644 --- a/README.md +++ b/README.md @@ -136,18 +136,21 @@ In general, it makes the whole experience of using org mode notebooks for infrsa This package works with "targets" defined at the top of the file, which also appear in heading tags: ```org -#+TARGET: /ssh:example1.com: (server1) -#+TARGET: /ssh:example2.com: (server2) +#+TARGET: server1 /ssh:example1.com: +#+TARGET: server2 /ssh:example2.com: * Do something on server1 :server1: * Do something on server2 :server2: ``` +Each `#+TARGET` names a tag and then its target, like org's `#+LINK`. The older +form, `#+TARGET: /ssh:example1.com: (server1)`, still works. + Source code blocks under a heading tag that matches a target (a "target tag") execute in on the server, instead of locally: ```org -#+TARGET: /ssh:example1.com: (server1) -#+TARGET: /ssh:example2.com: (server2) +#+TARGET: server1 /ssh:example1.com: +#+TARGET: server2 /ssh:example2.com: * Do something on server1 :server1: @@ -214,7 +217,7 @@ processing the output of the code block. For example, let's say we have a `TARGET` that points to a Podman container in a server: ```org -#+TARGET: /ssh:server.com|podman:my-container: (container) +#+TARGET: container /ssh:server.com|podman:my-container: ``` We can chain together a remote command and a local processing step as follows: @@ -242,7 +245,7 @@ This will work even if the `jq` command is not installed in the container :) A `#+TARGET` value can be a noweb-style reference: ```org -#+TARGET: <> (server) +#+TARGET: server <> #+name: input-instance : app1 @@ -302,7 +305,7 @@ When `devops-enable-session-async` is enabled, blocks are executed as if you had the `session` and `async` headers yourself. For example, ```org -#+TARGET: /ssh:example.com: (example) +#+TARGET: example /ssh:example.com: * Update packages :example: diff --git a/devops-test.el b/devops-test.el index c13032b..0ff2832 100644 --- a/devops-test.el +++ b/devops-test.el @@ -118,6 +118,31 @@ The directory is removed afterwards." "<> (server)") '("server" . "<>")))) +(ert-deftest devops--parse-target-keyword-link-style-test () + "A #+TARGET value may name its tag first, like #+LINK." + (should (equal (devops--parse-target-keyword "server1 /ssh:host1:") + '("server1" . "/ssh:host1:"))) + (should (equal (devops--parse-target-keyword "local /srv/app/") + '("local" . "/srv/app/"))) + (should (equal (devops--parse-target-keyword "server <>") + '("server" . "<>"))) + (should (equal (devops--parse-target-keyword + "server <>") + '("server" . "<>"))) + ;; A relative directory in the old form is not read as a tag. + (should (equal (devops--parse-target-keyword "app (local)") + '("local" . "app"))) + (should (null (devops--parse-target-keyword "server")))) + +(ert-deftest devops-target-tag-alist-mixed-test () + "Both #+TARGET forms may appear in one buffer." + (devops-test--with-org + (concat "#+TARGET: /srv/one/ (t1)\n" + "#+TARGET: t2 /srv/two/\n\n" + "* Heading\n") + (should (equal (devops-target-tag-alist) + '(("t1" . "/srv/one/") ("t2" . "/srv/two/")))))) + (ert-deftest devops-target-ref-test () "`devops-target-ref' tells a reference from a literal target." (should (equal (devops-target-ref "<>") "server-target()")) diff --git a/devops.el b/devops.el index 25a56b7..bbb3016 100644 --- a/devops.el +++ b/devops.el @@ -27,8 +27,8 @@ ;; ;; `devops.el' adds TARGET syntax to org-babel: ;; -;; #+TARGET: /ssh:web1.example.com: (web) -;; #+TARGET: /ssh:db1.example.com: (db) +;; #+TARGET: web /ssh:web1.example.com: +;; #+TARGET: db /ssh:db1.example.com: ;; ;; * Check the web server :web: ;; #+begin_src sh @@ -138,15 +138,17 @@ reference, a dynamic target, tangling, a drift check. Meaningful in `org-babel-after-execute-hook'." (not devops--inhibit-async)) -;; Maybe it would make sense to follow the #+LINK syntax? -;; #+TARGET: server /ssh:example1.com: - (defun devops--parse-target-keyword (value) - "Parse a #+TARGET VALUE like \"target1 (source)\" into (TAG . TARGET). + "Parse a #+TARGET VALUE into (TAG . TARGET). +VALUE is \"TAG TARGET\", like org's #+LINK, or the older \"TARGET (TAG)\". TARGET is a directory, a TRAMP prefix, or a reference to a named block written in noweb's brackets, `<>' or `<>'." - (when (string-match "\\`\\(<<.*>>\\|[^ ]+\\) +(\\([^)]+\\))\\'" value) - (cons (match-string 2 value) (match-string 1 value)))) + (let ((target "\\(<<.*>>\\|[^ ]+\\)")) + (cond + ((string-match (concat "\\`" target " +(\\([^)]+\\))\\'") value) + (cons (match-string 2 value) (match-string 1 value))) + ((string-match (concat "\\`\\([[:alnum:]_@#%]+\\) +" target "\\'") value) + (cons (match-string 1 value) (match-string 2 value)))))) (defun devops--org-keywords (key) "Return all values for keyword KEY as a list." diff --git a/examples/1_commands.org b/examples/1_commands.org index 5464fed..bc219a3 100644 --- a/examples/1_commands.org +++ b/examples/1_commands.org @@ -1,13 +1,13 @@ #+PROPERTY: header-args:sh :results output -#+TARGET: /ssh:example1.com: (server1) -#+TARGET: /ssh:example2.com: (server2) +#+TARGET: server1 /ssh:example1.com: +#+TARGET: server2 /ssh:example2.com: #+TITLE: Running server commands =devops.el= helps you run commands on one or more named targets by declaring them at the the top of the file. #+begin_example -#+TARGET: /ssh:example1.com: (server1) -#+TARGET: /ssh:example2.com: (server2) +#+TARGET: server1 /ssh:example1.com: +#+TARGET: server2 /ssh:example2.com: #+end_example Normally, org-babel runs commands on servers whenever the =:dir= header argument diff --git a/examples/2_tangle.org b/examples/2_tangle.org index ee599c5..deecdce 100644 --- a/examples/2_tangle.org +++ b/examples/2_tangle.org @@ -1,5 +1,5 @@ -#+TARGET: /ssh:example1.com: (server1) -#+TARGET: /ssh:example2.com: (server2) +#+TARGET: server1 /ssh:example1.com: +#+TARGET: server2 /ssh:example2.com: #+TITLE: Example 2: Tangle files =devops.el= helps you create files in remote servers. From fcd17d440543de98551099eb86fbd1a364510ff2 Mon Sep 17 00:00:00 2001 From: Kyle S Passarelli Date: Mon, 19 Oct 2026 20:47:15 -0600 Subject: [PATCH 2/2] Bind public devops-resolving-target while a dynamic target resolves A confirmation prompt can now tell that a block runs to locate a target, and for which tag. --- devops-test.el | 29 +++++++++++++++++++++++++++++ devops.el | 12 ++++++++++++ 2 files changed, 41 insertions(+) diff --git a/devops-test.el b/devops-test.el index 0ff2832..eb1b56b 100644 --- a/devops-test.el +++ b/devops-test.el @@ -240,6 +240,35 @@ The directory is removed afterwards." (let ((org-confirm-babel-evaluate nil)) (should-error (devops--resolve-target-for-tag "t1") :type 'user-error)))) +(defvar devops-test--seen nil + "Values of `devops-resolving-target' seen by test blocks, latest first.") + +(ert-deftest devops-resolving-target-test () + "`devops-resolving-target' is the innermost (TAG . REF) while resolving." + (devops-test--with-org + (concat "#+TARGET: t1 <>\n#+TARGET: t2 <>\n" + "#+TARGET: t3 <>\n\n" + "* A\t\t:t2:\n\n" + "#+name: a\n#+begin_src emacs-lisp\n" + "(push devops-resolving-target devops-test--seen)\n" + "temporary-file-directory\n#+end_src\n\n" + "* B\n\n" + "#+name: b\n#+begin_src emacs-lisp\n" + "(push devops-resolving-target devops-test--seen)\n" + "temporary-file-directory\n#+end_src\n\n" + "#+name: c\n#+begin_src emacs-lisp\n" + "(error \"Boom\")\n#+end_src\n") + (let ((org-confirm-babel-evaluate nil) + (devops-test--seen nil)) + (should-not devops-resolving-target) + ;; Resolving t1 runs a, under t2, which first resolves t2 by b. + (should (equal (devops--resolve-target-for-tag "t1") + temporary-file-directory)) + (should (equal devops-test--seen '(("t1" . "a()") ("t2" . "b()")))) + (should-not devops-resolving-target) + (should-error (devops--resolve-target-for-tag "t3")) + (should-not devops-resolving-target)))) + (ert-deftest devops-heading-targets-resolves-only-its-tags-test () "Only the references of tags on the heading are resolved." (devops-test--with-org diff --git a/devops.el b/devops.el index bbb3016..6ebc5db 100644 --- a/devops.el +++ b/devops.el @@ -68,6 +68,10 @@ ;; ;; - `devops-enable-session-async' run blocks in async session pool ;; +;; For packages that build on it: +;; +;; - `devops-resolving-target' (TAG . REF) while a dynamic target resolves +;; ;; Please report issues at ;; https://github.com/kpassapk/devops.el/issues @@ -167,6 +171,13 @@ written in noweb's brackets, `<>' or `<>'." (defvar devops--resolving-tags nil "Tags whose target references are being resolved, innermost first.") +(defvar devops-resolving-target nil + "The dynamic target being resolved, as (TAG . REF), or nil. +Bound while `devops--target-from-ref' runs REF, the reference in TAG's +#+TARGET keyword, so code called from it, such as a confirmation +prompt, can tell the block runs to locate a target. When resolutions +nest, it holds the innermost.") + (defun devops--target-value-string (value) "Return VALUE, a resolved reference, as a trimmed string, or nil. A shell block with `:results value' hands its one line back as a @@ -188,6 +199,7 @@ value." (user-error "Target %s: %s is a src block; write <<%s()>> to run it" tag ref ref)) (let* ((devops--resolving-tags (cons tag devops--resolving-tags)) + (devops-resolving-target (cons tag ref)) (value (devops--target-value-string (save-excursion (org-babel-ref-resolve ref))))) (when (or (null value)