Skip to content

Commit 071b288

Browse files
Merge pull request #39 from actionforge/p4
Add nodes for Perforce P4
2 parents 5e94f52 + 32d9b02 commit 071b288

25 files changed

+981
-23
lines changed

.github/workflows/workflow.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,17 @@ jobs:
101101
runner-path: ${{ github.workspace }}/actrun
102102
graph-file: build-test-publish.act
103103
inputs: ${{ toJson(inputs) }}
104-
secrets: ${{ toJson(secrets) }}
104+
secrets: >-
105+
{
106+
"APPLE_P12_CERTIFICATE_PASSWORD": "${{ secrets.APPLE_P12_CERTIFICATE_PASSWORD }}",
107+
"APPLE_P12_CERTIFICATE_BASE64": "${{ secrets.APPLE_P12_CERTIFICATE_BASE64 }}",
108+
"TESTE2E_DO_S3_ACCESS_KEY": "${{ secrets.TESTE2E_DO_S3_ACCESS_KEY }}",
109+
"TESTE2E_DO_S3_SECRET_KEY": "${{ secrets.TESTE2E_DO_S3_SECRET_KEY }}",
110+
"TESTE2E_AWS_S3_ACCESS_KEY": "${{ secrets.TESTE2E_AWS_S3_ACCESS_KEY }}",
111+
"TESTE2E_AWS_S3_SECRET_KEY": "${{ secrets.TESTE2E_AWS_S3_SECRET_KEY }}",
112+
"PUBLISH_S3_SECRET_KEY": "${{ secrets.PUBLISH_S3_SECRET_KEY }}",
113+
"PUBLISH_S3_ACCESS_KEY": "${{ secrets.PUBLISH_S3_ACCESS_KEY }}"
114+
}
105115
matrix: ${{ toJson(matrix) }}
106116

107117
docker-manifest:

agent/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
type Client struct {
1313
serverURL string
1414
token string
15+
uuid string
1516
httpClient *http.Client
1617
}
1718

@@ -25,6 +26,10 @@ func NewClient(serverURL, token string) *Client {
2526
}
2627
}
2728

29+
func (c *Client) SetUUID(uuid string) {
30+
c.uuid = uuid
31+
}
32+
2833
func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
2934
var bodyReader io.Reader
3035
if body != nil {
@@ -40,6 +45,9 @@ func (c *Client) doRequest(method, path string, body interface{}) (*http.Respons
4045
return nil, err
4146
}
4247
req.Header.Set("Authorization", "Bearer "+c.token)
48+
if c.uuid != "" {
49+
req.Header.Set("X-Agent-UUID", c.uuid)
50+
}
4351
if body != nil {
4452
req.Header.Set("Content-Type", "application/json")
4553
}

agent/flock_unix.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build !windows
2+
3+
package agent
4+
5+
import (
6+
"os"
7+
"syscall"
8+
)
9+
10+
func lockFileExclusive(f *os.File) error {
11+
return syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
12+
}
13+
14+
func unlockFile(f *os.File) {
15+
_ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
16+
}

agent/flock_windows.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//go:build windows
2+
3+
package agent
4+
5+
import (
6+
"os"
7+
"syscall"
8+
"unsafe"
9+
)
10+
11+
var (
12+
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
13+
procLockFileEx = modkernel32.NewProc("LockFileEx")
14+
procUnlockFileEx = modkernel32.NewProc("UnlockFileEx")
15+
)
16+
17+
const (
18+
lockfileExclusiveLock = 0x00000002
19+
lockfileFailImmediately = 0x00000001
20+
)
21+
22+
func lockFileExclusive(f *os.File) error {
23+
var overlapped syscall.Overlapped
24+
r1, _, err := procLockFileEx.Call(
25+
uintptr(f.Fd()),
26+
uintptr(lockfileExclusiveLock|lockfileFailImmediately),
27+
0,
28+
1, 0,
29+
uintptr(unsafe.Pointer(&overlapped)),
30+
)
31+
if r1 == 0 {
32+
return err
33+
}
34+
return nil
35+
}
36+
37+
func unlockFile(f *os.File) {
38+
var overlapped syscall.Overlapped
39+
procUnlockFileEx.Call(
40+
uintptr(f.Fd()),
41+
0,
42+
1, 0,
43+
uintptr(unsafe.Pointer(&overlapped)),
44+
)
45+
}

agent/vcs/p4.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (p *P4Provider) Checkout(ctx context.Context, url, ref, pipeline, destDir s
9393
return CheckoutResult{}, fmt.Errorf("p4 sync pipeline file failed: %w", err)
9494
}
9595

96-
return CheckoutResult{Dir: root, Persistent: true}, nil
96+
return CheckoutResult{Dir: root, Persistent: true, P4Client: p.clientName}, nil
9797
}
9898

9999
// Create temporary workspace
@@ -132,7 +132,7 @@ func (p *P4Provider) Checkout(ctx context.Context, url, ref, pipeline, destDir s
132132
return CheckoutResult{}, fmt.Errorf("p4 sync pipeline file failed: %w", err)
133133
}
134134

135-
return CheckoutResult{Dir: absDir}, nil
135+
return CheckoutResult{Dir: absDir, Persistent: true, P4Client: p.clientName}, nil
136136
}
137137

138138
func (p *P4Provider) Cleanup(ctx context.Context) error {

agent/vcs/vcs.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type CheckoutResult struct {
3333
Persistent bool
3434
// SHA is the resolved commit SHA (or changelist number for P4) after checkout.
3535
SHA string
36+
// P4Client is the Perforce workspace name created or reused during checkout.
37+
// The worker should set P4CLIENT in the subprocess environment so that
38+
// p4 commands within the graph can operate on the same workspace.
39+
P4Client string
3640
}
3741

3842
// Provider handles VCS checkout operations.

agent/worker.go

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,59 +26,105 @@ type Worker struct {
2626
vcsOpts vcs.Options
2727
pollInterval time.Duration
2828
uuid string
29+
slotCleanup func()
2930
log *logrus.Entry
3031

3132
metricsMu sync.Mutex
3233
lastCounters *RawCounters
3334
}
3435

3536
func NewWorker(client *Client, docker DockerConfig, vcsOpts vcs.Options) *Worker {
37+
uuid, cleanup := acquireAgentSlot()
38+
client.SetUUID(uuid)
3639
return &Worker{
3740
client: client,
3841
docker: docker,
3942
vcsOpts: vcsOpts,
4043
pollInterval: 1 * time.Second,
41-
uuid: loadOrGenerateUUID(),
44+
uuid: uuid,
45+
slotCleanup: cleanup,
4246
log: logrus.WithField("component", "agent"),
4347
}
4448
}
4549

46-
// uuidFilePath returns the path to the persistent UUID file in the user's config directory.
47-
func uuidFilePath() string {
50+
// agentSlotDir returns the directory for agent UUID slot files.
51+
func agentSlotDir() string {
4852
dir, err := os.UserConfigDir()
4953
if err != nil {
5054
dir = os.TempDir()
5155
}
52-
return filepath.Join(dir, "actionforge", "agent-uuid")
56+
return filepath.Join(dir, "actionforge")
5357
}
5458

55-
// loadOrGenerateUUID loads a persistent UUID from disk, or generates and saves a new one.
56-
func loadOrGenerateUUID() string {
57-
path := uuidFilePath()
58-
if data, err := os.ReadFile(path); err == nil {
59-
if id := strings.TrimSpace(string(data)); len(id) == 36 {
60-
return id
59+
// acquireAgentSlot finds and locks the lowest available agent slot.
60+
// Each slot has a persistent UUID file and a lock file. When the process
61+
// exits, the lock is released so the next process can reuse that slot
62+
// (and its UUID/metrics history).
63+
// Returns the UUID and a cleanup function that releases the lock.
64+
func acquireAgentSlot() (string, func()) {
65+
dir := agentSlotDir()
66+
_ = os.MkdirAll(dir, 0700)
67+
68+
const maxSlots = 256
69+
for i := 0; i < maxSlots; i++ {
70+
lockPath := filepath.Join(dir, fmt.Sprintf("agent-%d.lock", i))
71+
uuidPath := filepath.Join(dir, fmt.Sprintf("agent-%d.uuid", i))
72+
73+
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
74+
if err != nil {
75+
continue
76+
}
77+
78+
if err := lockFileExclusive(lockFile); err != nil {
79+
if cerr := lockFile.Close(); cerr != nil {
80+
logrus.WithError(cerr).Warn("failed to close lock file")
81+
}
82+
continue
6183
}
84+
85+
// Slot acquired — read or generate UUID
86+
uuid := ""
87+
if data, err := os.ReadFile(uuidPath); err == nil {
88+
if id := strings.TrimSpace(string(data)); len(id) == 36 {
89+
uuid = id
90+
}
91+
}
92+
if uuid == "" {
93+
var buf [16]byte
94+
_, _ = rand.Read(buf[:])
95+
buf[6] = (buf[6] & 0x0f) | 0x40 // version 4
96+
buf[8] = (buf[8] & 0x3f) | 0x80 // variant 1
97+
uuid = fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
98+
buf[0:4], buf[4:6], buf[6:8], buf[8:10], buf[10:16])
99+
_ = os.WriteFile(uuidPath, []byte(uuid+"\n"), 0600)
100+
}
101+
102+
cleanup := func() {
103+
unlockFile(lockFile)
104+
if cerr := lockFile.Close(); cerr != nil {
105+
logrus.WithError(cerr).Warn("failed to close lock file")
106+
}
107+
}
108+
return uuid, cleanup
62109
}
63110

64-
// Generate UUID v4
111+
// Fallback: all slots taken, generate ephemeral UUID with no lock
65112
var buf [16]byte
66113
_, _ = rand.Read(buf[:])
67-
buf[6] = (buf[6] & 0x0f) | 0x40 // version 4
68-
buf[8] = (buf[8] & 0x3f) | 0x80 // variant 1
69-
id := fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
70-
buf[0:4], buf[4:6], buf[6:8], buf[8:10], buf[10:16])
71-
72-
_ = os.MkdirAll(filepath.Dir(path), 0700)
73-
_ = os.WriteFile(path, []byte(id+"\n"), 0600)
74-
return id
114+
buf[6] = (buf[6] & 0x0f) | 0x40
115+
buf[8] = (buf[8] & 0x3f) | 0x80
116+
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
117+
buf[0:4], buf[4:6], buf[6:8], buf[8:10], buf[10:16]), func() {}
75118
}
76119

77120
// maxConsecutiveErrors is the number of consecutive connection errors before
78121
// Run returns ErrConnectionLost so the caller can decide to restart.
79122
const maxConsecutiveErrors = 10
80123

81124
func (w *Worker) Run(ctx context.Context) error {
125+
if w.slotCleanup != nil {
126+
defer w.slotCleanup()
127+
}
82128
w.log.Info("starting")
83129

84130
// Take initial snapshot for delta computation
@@ -358,12 +404,16 @@ func (w *Worker) execute(ctx context.Context, job *ClaimResponse) {
358404
env = append(env, "BUILD_TMPDIR="+tmpDir)
359405
env = append(env, "BUILD_VCS_TYPE="+job.VCSType)
360406
env = append(env, "BUILD_VCS_URL="+job.VCSURL)
407+
env = append(env, "BUILD_REF="+ref)
361408
if job.RepoID != "" {
362409
env = append(env, "BUILD_REPO_ID="+job.RepoID)
363410
}
364411
if checkout.SHA != "" {
365412
env = append(env, "BUILD_COMMIT_SHA="+checkout.SHA)
366413
}
414+
if checkout.P4Client != "" {
415+
env = append(env, "P4CLIENT="+checkout.P4Client)
416+
}
367417

368418
// Resolve env mappings from trigger config (if present)
369419
if len(job.EnvMappings) > 0 && job.MatrixValues != nil {

core/base.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const (
4949
CredentialTypeSSH CredentialType = iota
5050
CredentialTypeUsernamePassword
5151
CredentialTypeAccessKey
52+
CredentialTypeP4
5253
)
5354

5455
type Credentials interface {

node_interfaces/interface_core_p4-credentials_v1.go

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_interfaces/interface_core_p4-print_v1.go

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)