Skip to content

Commit 562497a

Browse files
committed
Add P4 nodes: p4-credentials, p4-sync, p4-print, p4-run
All nodes use the p4go library (requires -tags p4 build flag). Credentials are resolved from node inputs with fallback to P4PORT, P4USER, P4PASSWD, P4TRUST, P4CLIENT environment variables. SSL trust is handled automatically via p4go before each operation.
1 parent 629b7e1 commit 562497a

9 files changed

Lines changed: 672 additions & 0 deletions

nodes/[email protected]

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//go:build p4
2+
3+
package nodes
4+
5+
import (
6+
_ "embed"
7+
8+
"github.com/actionforge/actrun-cli/core"
9+
ni "github.com/actionforge/actrun-cli/node_interfaces"
10+
)
11+
12+
13+
var p4CredentialsDefinition string
14+
15+
type P4CredentialsNode struct {
16+
core.NodeBaseComponent
17+
core.Inputs
18+
core.Outputs
19+
}
20+
21+
func (n *P4CredentialsNode) OutputValueById(c *core.ExecutionState, outputId core.OutputId) (any, error) {
22+
if outputId != ni.Core_p4_credentials_v1_Output_credential {
23+
return nil, core.CreateErr(c, nil, "unknown output id '%s'", outputId)
24+
}
25+
26+
port, _ := core.InputValueById[core.SecretValue](c, n, ni.Core_p4_credentials_v1_Input_port)
27+
user, _ := core.InputValueById[core.SecretValue](c, n, ni.Core_p4_credentials_v1_Input_user)
28+
password, _ := core.InputValueById[core.SecretValue](c, n, ni.Core_p4_credentials_v1_Input_password)
29+
trust, _ := core.InputValueById[string](c, n, ni.Core_p4_credentials_v1_Input_trust)
30+
client, _ := core.InputValueById[string](c, n, ni.Core_p4_credentials_v1_Input_client)
31+
32+
return P4Credentials{
33+
Port: port.Secret,
34+
User: user.Secret,
35+
Password: password.Secret,
36+
Trust: trust,
37+
Client: client,
38+
}, nil
39+
}
40+
41+
func init() {
42+
err := core.RegisterNodeFactory(p4CredentialsDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool, opts core.RunOpts) (core.NodeBaseInterface, []error) {
43+
return &P4CredentialsNode{}, nil
44+
})
45+
if err != nil {
46+
panic(err)
47+
}
48+
}

nodes/[email protected]

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
yaml-version: 3.0
2+
id: core/p4-credentials
3+
name: P4 Credentials
4+
version: 1
5+
category: perforce
6+
icon: tablerServer
7+
style:
8+
header:
9+
background: "#07a3ee"
10+
color: "#ffffff"
11+
body:
12+
background: "#055f8a"
13+
short_desc: Bundles Perforce P4 connection details into a reusable credential object.
14+
addendum: |
15+
This data node holds Perforce P4 server connection details and outputs them as a credential object.
16+
17+
Connect the `credential` output to P4 nodes that accept a `credentials` input.
18+
19+
When fields are left empty, the corresponding P4 environment variables are used as fallback at execution time.
20+
compact: true
21+
outputs:
22+
credential:
23+
type: credentials
24+
desc: A credential object containing the Perforce P4 connection details.
25+
index: 0
26+
inputs:
27+
port:
28+
type: secret
29+
name: P4PORT
30+
hint: "ssl:perforce.example.com:1666"
31+
desc: The Perforce server address. Falls back to P4PORT env var if empty.
32+
hide_socket: true
33+
index: 0
34+
user:
35+
type: secret
36+
name: P4USER
37+
hint: "joe"
38+
desc: The Perforce username. Falls back to P4USER env var if empty.
39+
hide_socket: true
40+
index: 1
41+
password:
42+
type: secret
43+
name: P4PASSWD
44+
desc: The Perforce password or ticket. Falls back to P4PASSWD env var if empty.
45+
hide_socket: true
46+
index: 2
47+
trust:
48+
type: string
49+
name: P4TRUST
50+
desc: Path to the P4 trust file. Falls back to P4TRUST env var if empty.
51+
optional: true
52+
index: 3
53+
client:
54+
type: string
55+
name: P4CLIENT
56+
hint: "my-workspace"
57+
desc: The Perforce workspace/client name. Falls back to P4CLIENT env var if empty.
58+
optional: true
59+
index: 4

nodes/[email protected]

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//go:build p4
2+
3+
package nodes
4+
5+
import (
6+
_ "embed"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/actionforge/actrun-cli/core"
11+
ni "github.com/actionforge/actrun-cli/node_interfaces"
12+
)
13+
14+
15+
var p4PrintDefinition string
16+
17+
type P4PrintNode struct {
18+
core.NodeBaseComponent
19+
core.Executions
20+
core.Inputs
21+
core.Outputs
22+
}
23+
24+
func (n *P4PrintNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, prevError error) error {
25+
depotPath, err := core.InputValueById[string](c, n, ni.Core_p4_print_v1_Input_depot_path)
26+
if err != nil {
27+
return err
28+
}
29+
30+
outputPath, err := core.InputValueById[string](c, n, ni.Core_p4_print_v1_Input_output_path)
31+
if err != nil {
32+
return err
33+
}
34+
35+
creds, _ := core.InputValueById[core.Credentials](c, n, ni.Core_p4_print_v1_Input_credentials)
36+
37+
fields := buildP4Fields(c, creds)
38+
39+
p4, err := connectP4(c, fields)
40+
if err != nil {
41+
return n.Execute(ni.Core_p4_print_v1_Output_exec_err, c, err)
42+
}
43+
defer func() {
44+
p4.Disconnect()
45+
p4.Close()
46+
}()
47+
48+
// Ensure parent directory exists
49+
if dir := filepath.Dir(outputPath); dir != "" {
50+
if err := os.MkdirAll(dir, 0755); err != nil {
51+
mkdirErr := core.CreateErr(c, err, "failed to create output directory '%s'", dir)
52+
return n.Execute(ni.Core_p4_print_v1_Output_exec_err, c, mkdirErr)
53+
}
54+
}
55+
56+
// p4 print -q -o <output> <depot_path>
57+
_, runErr := p4.Run("print", "-q", "-o", outputPath, depotPath)
58+
if runErr != nil {
59+
printErr := core.CreateErr(c, runErr, "p4 print failed for %s", depotPath)
60+
return n.Execute(ni.Core_p4_print_v1_Output_exec_err, c, printErr)
61+
}
62+
63+
return n.Execute(ni.Core_p4_print_v1_Output_exec_success, c, nil)
64+
}
65+
66+
func init() {
67+
err := core.RegisterNodeFactory(p4PrintDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool, opts core.RunOpts) (core.NodeBaseInterface, []error) {
68+
return &P4PrintNode{}, nil
69+
})
70+
if err != nil {
71+
panic(err)
72+
}
73+
}

nodes/[email protected]

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
yaml-version: 3.0
2+
id: core/p4-print
3+
name: P4 Print
4+
version: 1
5+
category: perforce
6+
icon: tablerFileDownload
7+
style:
8+
header:
9+
background: "#07a3ee"
10+
color: "#ffffff"
11+
body:
12+
background: "#055f8a"
13+
short_desc: Downloads a file from the Perforce P4 depot without requiring a workspace.
14+
addendum: |
15+
Runs `p4 print -q -o <output_path> <depot_path>` to download a single file from the depot directly to disk.
16+
17+
This does not require a workspace (P4CLIENT) to be set, making it useful for fetching individual files.
18+
19+
For SSL servers, `p4 trust -y` is automatically executed before the command.
20+
outputs:
21+
exec-success:
22+
name: Success
23+
exec: true
24+
desc: Executes if the file is downloaded successfully.
25+
index: 0
26+
exec-err:
27+
name: Error
28+
exec: true
29+
desc: Executes if there is an error downloading the file.
30+
index: 1
31+
inputs:
32+
exec:
33+
exec: true
34+
index: 0
35+
depot_path:
36+
type: string
37+
name: Depot Path
38+
hint: "//depot/project/README.md"
39+
desc: The depot path of the file to download.
40+
index: 1
41+
required: true
42+
output_path:
43+
type: string
44+
name: Output Path
45+
desc: The local file path where the downloaded file will be saved.
46+
index: 2
47+
required: true
48+
credentials:
49+
type: credentials
50+
name: Credentials
51+
desc: Optional P4 credentials. If not provided, falls back to P4 environment variables.
52+
index: 3
53+
optional: true

nodes/[email protected]

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//go:build p4
2+
3+
package nodes
4+
5+
import (
6+
_ "embed"
7+
8+
"github.com/actionforge/actrun-cli/core"
9+
ni "github.com/actionforge/actrun-cli/node_interfaces"
10+
)
11+
12+
13+
var p4RunDefinition string
14+
15+
type P4RunNode struct {
16+
core.NodeBaseComponent
17+
core.Executions
18+
core.Inputs
19+
core.Outputs
20+
}
21+
22+
func (n *P4RunNode) ExecuteImpl(c *core.ExecutionState, inputId core.InputId, prevError error) error {
23+
command, err := core.InputValueById[string](c, n, ni.Core_p4_run_v1_Input_command)
24+
if err != nil {
25+
return err
26+
}
27+
28+
extraArgs, _ := core.InputValueById[[]string](c, n, ni.Core_p4_run_v1_Input_args)
29+
creds, _ := core.InputValueById[core.Credentials](c, n, ni.Core_p4_run_v1_Input_credentials)
30+
31+
fields := buildP4Fields(c, creds)
32+
33+
p4, err := connectP4(c, fields)
34+
if err != nil {
35+
_ = n.SetOutputValue(c, ni.Core_p4_run_v1_Output_exit_code, 1, core.SetOutputValueOpts{})
36+
return n.Execute(ni.Core_p4_run_v1_Output_exec_err, c, err)
37+
}
38+
defer func() {
39+
p4.Disconnect()
40+
p4.Close()
41+
}()
42+
43+
results, runErr := p4.Run(command, extraArgs...)
44+
output := formatP4Results(results)
45+
46+
_ = n.SetOutputValue(c, ni.Core_p4_run_v1_Output_output, output, core.SetOutputValueOpts{})
47+
48+
if runErr != nil {
49+
_ = n.SetOutputValue(c, ni.Core_p4_run_v1_Output_exit_code, 1, core.SetOutputValueOpts{})
50+
cmdErr := core.CreateErr(c, runErr, "p4 %s failed", command)
51+
return n.Execute(ni.Core_p4_run_v1_Output_exec_err, c, cmdErr)
52+
}
53+
54+
_ = n.SetOutputValue(c, ni.Core_p4_run_v1_Output_exit_code, 0, core.SetOutputValueOpts{})
55+
return n.Execute(ni.Core_p4_run_v1_Output_exec_success, c, nil)
56+
}
57+
58+
func init() {
59+
err := core.RegisterNodeFactory(p4RunDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool, opts core.RunOpts) (core.NodeBaseInterface, []error) {
60+
return &P4RunNode{}, nil
61+
})
62+
if err != nil {
63+
panic(err)
64+
}
65+
}

nodes/[email protected]

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
yaml-version: 3.0
2+
id: core/p4-run
3+
name: P4 Run
4+
version: 1
5+
category: perforce
6+
icon: tablerTerminal
7+
style:
8+
header:
9+
background: "#07a3ee"
10+
color: "#ffffff"
11+
body:
12+
background: "#055f8a"
13+
short_desc: Runs an arbitrary Perforce P4 command.
14+
addendum: |
15+
Executes any Perforce command via the `p4` CLI. Use this for commands not covered by dedicated P4 nodes (e.g. `p4 files`, `p4 changes`, `p4 client`).
16+
17+
The `command` input is the p4 subcommand (e.g. "files", "changes", "describe").
18+
Additional arguments go into the `args` input.
19+
20+
For SSL servers, `p4 trust -y` is automatically executed before the command.
21+
outputs:
22+
exec-success:
23+
name: Success
24+
exec: true
25+
desc: Executes if the command succeeds (exit code 0).
26+
index: 0
27+
exec-err:
28+
name: Error
29+
exec: true
30+
desc: Executes if the command fails (non-zero exit code).
31+
index: 1
32+
output:
33+
type: string
34+
name: Output
35+
desc: The combined stdout/stderr output from the p4 command.
36+
index: 2
37+
exit_code:
38+
name: Exit Code
39+
type: number
40+
desc: The exit code of the p4 command.
41+
index: 3
42+
inputs:
43+
exec:
44+
exec: true
45+
index: 0
46+
command:
47+
type: string
48+
name: Command
49+
hint: "files"
50+
desc: The p4 subcommand to run (e.g. files, changes, describe, client).
51+
index: 1
52+
required: true
53+
args:
54+
name: Args
55+
type: "[]string"
56+
desc: Additional arguments for the p4 command.
57+
index: 2
58+
optional: true
59+
credentials:
60+
type: credentials
61+
name: Credentials
62+
desc: Optional P4 credentials. If not provided, falls back to P4 environment variables.
63+
index: 3
64+
optional: true

0 commit comments

Comments
 (0)