Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
292f8b5
Update dependencies: grpc, circl, and add vault/p4 support libraries
sebastianrath Mar 27, 2026
e5a5b48
Add NodeStateCallback to core execution and improve workflow types
sebastianrath Mar 27, 2026
2a3c422
Add agent package for remote job execution
sebastianrath Mar 27, 2026
8163f99
Add artifact and workspace nodes with agent CLI command
sebastianrath Mar 27, 2026
52b1a80
Add Perforce API headers, game build example, and P4 docs
sebastianrath Mar 27, 2026
f0e7ae1
Fix path traversal, goroutine leak, and data race in agent subsystem
sebastianrath Mar 27, 2026
044bbf3
Run go mod tidy
sebastianrath Mar 27, 2026
9048bc5
Improve error handling and response body logging in artifact upload
sebastianrath Mar 28, 2026
b54c6ce
Remove time limit from upload
sebastianrath Mar 28, 2026
0b9831b
Add Docker image build and publish step for Linux tag pushes
sebastianrath Mar 28, 2026
0735922
Add Windows metrics collection for CPU, mem and network stats
sebastianrath Mar 28, 2026
82390bb
Add platform switch node for Docker image publishing
sebastianrath Mar 28, 2026
5eb6ebe
Update tests
sebastianrath Mar 28, 2026
8d340e9
Add package permissions
sebastianrath Mar 28, 2026
f8bca9a
Push docker on workflow_dispatch and for x64 and arm64
sebastianrath Mar 28, 2026
84cc2b0
Switch base image from Alpine to Ubuntu 24.04 and add pwsh
sebastianrath Mar 28, 2026
e320468
Use go-git as GitProvider
sebastianrath Mar 28, 2026
2da8a7d
Implement orchestrator support for workspace downloads and uploads
sebastianrath Mar 29, 2026
7b28d82
Improve SendLogs method to return job status and handle errors more g…
sebastianrath Mar 29, 2026
6a2cb9c
Cleanup support for p4go
sebastianrath Mar 29, 2026
8893563
Fetch only pipeline file from VCS instead of full repo checkout
sebastianrath Apr 1, 2026
3a1c3c6
Rename workspace-download to repo-download and add artifact alias sup…
sebastianrath Apr 1, 2026
603d9a1
Add libssl-dev to build deps in Dockerfile
sebastianrath Apr 1, 2026
6e0001c
Enhance SSL support for p4 provider
sebastianrath Apr 2, 2026
b821f35
Minor fix in workflow graph
sebastianrath Apr 5, 2026
d602f56
Fix workflow
sebastianrath Apr 5, 2026
e4d989d
Add static OpenSSL build support and update Dockerfile dependencies
sebastianrath Apr 5, 2026
3f44233
Add OpenSSL installation to GCC setup for cross-compilation
sebastianrath Apr 5, 2026
e51390d
Fix YAML folding compatibility and Linux arm64 OpenSSL linking
sebastianrath Apr 5, 2026
1c1c462
Add debug output
sebastianrath Apr 6, 2026
cdf36e6
Update e2e test messages and adjust Windows ARM64 handling in setup s…
sebastianrath Apr 6, 2026
b354fbd
Add debug output for publish and package steps in build workflow
sebastianrath Apr 6, 2026
5bc68b3
Remove unused node connections in build workflow
sebastianrath Apr 6, 2026
35493f6
Support native ARM64 runners and multi-arch Docker manifests
sebastianrath Apr 6, 2026
e60d1c5
Refactor build workflow to enhance Linux architecture handling and im…
sebastianrath Apr 7, 2026
029dc24
Add inner architecture selection for Linux in build workflow
sebastianrath Apr 7, 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
455 changes: 356 additions & 99 deletions .github/workflows/graphs/build-test-publish.act

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ permissions:
# TODO: (Seb) Use fine-grained permissions as
# we only need this for Anchore SBOM Action
contents: write
packages: write

jobs:
build-quick:
Expand Down Expand Up @@ -72,7 +73,7 @@ jobs:
strategy:
matrix:
license: [free] # add pro when ready
os: [ubuntu-latest, windows-latest, macos-latest]
os: [windows-latest, ubuntu-latest, ubuntu-24.04-arm, macos-latest]

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -101,4 +102,23 @@ jobs:
graph-file: build-test-publish.act
inputs: ${{ toJson(inputs) }}
secrets: ${{ toJson(secrets) }}
matrix: ${{ toJson(matrix) }}
matrix: ${{ toJson(matrix) }}

docker-manifest:
name: Create Docker Multi-Arch Manifest
needs: build-test-publish
if: startsWith(github.ref, 'refs/tags/') && (github.event_name == 'workflow_dispatch' || (github.event_name == 'push'))
runs-on: ubuntu-latest
steps:
- name: Create multi-arch manifest
run: |
IMAGE="ghcr.io/actionforge/actrun"
VERSION="${GITHUB_REF_NAME}"

echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

docker buildx imagetools create \
-t "$IMAGE:$VERSION" \
-t "$IMAGE:latest" \
"$IMAGE:${VERSION}-x64" \
"$IMAGE:${VERSION}-arm64"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ __pycache__/
.DS_Store

tests_e2e/coverage
tests_e2e/coverage.html

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
Expand Down
53 changes: 48 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
## Build
##

FROM golang:1.25.0-alpine3.22 AS build
FROM golang:1.25.0 AS build

ARG TARGETARCH

RUN apt-get update && apt-get install -y --no-install-recommends gcc g++ libssl-dev && rm -rf /var/lib/apt/lists/*

WORKDIR /app

Expand All @@ -12,18 +16,57 @@ RUN go mod download

COPY . ./

RUN go build -o ./bin/actrun
RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "x64") && \
bash setup.sh linux "$ARCH" && \
P4_INCLUDE="$(pwd)/p4api/include" && \
if [ "$TARGETARCH" = "arm64" ]; then P4_LIB="$(pwd)/p4api/linux-aarch64/lib"; \
else P4_LIB="$(pwd)/p4api/linux-x86_64/lib"; fi && \
CGO_ENABLED=1 \
CGO_CPPFLAGS="-I$P4_INCLUDE" \
CGO_LDFLAGS="-L$P4_LIB -lp4api -lssl -lcrypto" \
go build -tags=p4 -o ./bin/actrun

##
## Deploy
##

FROM alpine:3.22.0
FROM ubuntu:24.04

LABEL org.opencontainers.image.title="Graph Runner"
LABEL org.opencontainers.image.description="Execution runtime for action graphs."
LABEL org.opencontainers.image.version={{img.version}}
LABEL org.opencontainers.image.source={{img.source}}
ARG IMG_VERSION=dev
ARG IMG_SOURCE=https://github.com/actionforge/actrun-cli

LABEL org.opencontainers.image.version=${IMG_VERSION}
LABEL org.opencontainers.image.source=${IMG_SOURCE}

ARG TARGETARCH

RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
locales \
curl \
wget \
jq \
zip \
unzip \
tar \
xz-utils \
python3 \
libicu74 libssl3t64 \
&& PWSH_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "x64") \
&& curl -fsSL "https://github.com/PowerShell/PowerShell/releases/download/v7.5.5/powershell-7.5.5-linux-${PWSH_ARCH}.tar.gz" \
-o /tmp/pwsh.tar.gz \
&& mkdir -p /opt/microsoft/powershell/7 \
&& tar -xzf /tmp/pwsh.tar.gz -C /opt/microsoft/powershell/7 \
&& chmod +x /opt/microsoft/powershell/7/pwsh \
&& ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \
&& rm /tmp/pwsh.tar.gz \
&& sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen && locale-gen \
&& rm -rf /var/lib/apt/lists/*

ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

COPY --from=build /app/bin /bin

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,26 @@ actrun --concurrency=false ./sequential_task.act

```

## 🔧 Perforce Support (Optional)

To build with Perforce (P4) support, you need the Perforce C/C++ API and OpenSSL installed.

1. Download the [Helix C/C++ API](https://www.perforce.com/downloads/helix-core-c/c++-api) and place it in `p4api/<os>/` (e.g. `p4api/macos/`).

2. Set the required environment variables before building:

```bash
export CGO_CPPFLAGS="-I$(pwd)/p4api/macos/include -g"
export CGO_LDFLAGS="-Wl,-no_warn_duplicate_libraries -L$(pwd)/p4api/macos/lib -L/opt/homebrew/opt/openssl@1.1/lib -lp4api -lssl -lcrypto -framework ApplicationServices -framework Foundation -framework Security"
export CGO_ENABLED=1
```

3. Build or run with the `p4` tag:

```bash
go run -tags p4 . agent --token=<your-token> --server=<server-url>
```

## 🛠️ Development Commands

If you are contributing to the core nodes or the CLI itself, the `dev` subcommand provides utilities to maintain the internal registry.
Expand Down
161 changes: 161 additions & 0 deletions agent/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package agent

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

type Client struct {
serverURL string
token string
httpClient *http.Client
}

func NewClient(serverURL, token string) *Client {
return &Client{
serverURL: serverURL,
token: token,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}

func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(data)
}

req, err := http.NewRequest(method, c.serverURL+path, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return c.httpClient.Do(req)
}

func (c *Client) ServerURL() string {
return c.serverURL
}

func (c *Client) Token() string {
return c.token
}

func (c *Client) Claim() (*ClaimResponse, error) {
resp, err := c.doRequest("POST", "/api/v2/ci/runner/claim", nil)
if err != nil {
return nil, fmt.Errorf("claim request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNoContent {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("claim failed: %s %s", resp.Status, string(body))
}

var claim ClaimResponse
if err := json.NewDecoder(resp.Body).Decode(&claim); err != nil {
return nil, fmt.Errorf("decode claim response: %w", err)
}
return &claim, nil
}

// drainAndCheck reads the response body to allow connection reuse and returns
// an error if the status code is not in the 2xx range.
func drainAndCheck(resp *http.Response) error {
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("unexpected status: %s", resp.Status)
}
return nil
}

// SendLogs sends a batch of log lines and returns the current job status from the server.
func (c *Client) SendLogs(jobID string, batch LogBatch) (string, error) {
resp, err := c.doRequest("POST", fmt.Sprintf("/api/v2/ci/runner/logs/%s", jobID), batch)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
_, _ = io.Copy(io.Discard, resp.Body)
return "", fmt.Errorf("unexpected status: %s", resp.Status)
}
var result struct {
Status string `json:"status"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", nil // non-fatal: old server without status in response
}
return result.Status, nil
}

func (c *Client) ReportStatus(jobID string, status RunStatus, exitCode *int) error {
report := StatusReport{
Status: status,
ExitCode: exitCode,
}
resp, err := c.doRequest("PATCH", fmt.Sprintf("/api/v2/ci/runner/jobs/%s", jobID), report)
if err != nil {
return err
}
return drainAndCheck(resp)
}

func (c *Client) SubmitGraph(jobID string, graph string) error {
resp, err := c.doRequest("POST", fmt.Sprintf("/api/v2/ci/runner/jobs/%s/graph", jobID), map[string]string{"graph": graph})
if err != nil {
return err
}
return drainAndCheck(resp)
}

func (c *Client) ReportRef(jobID, commitSHA string) error {
resp, err := c.doRequest("POST", fmt.Sprintf("/api/v2/ci/runner/jobs/%s/ref", jobID), map[string]string{"commit_sha": commitSHA})
if err != nil {
return err
}
return drainAndCheck(resp)
}

func (c *Client) SubmitActiveNodes(jobID string, nodes []ActiveNode) error {
resp, err := c.doRequest("POST", fmt.Sprintf("/api/v2/ci/runner/jobs/%s/nodes", jobID), map[string]interface{}{"active_nodes": nodes})
if err != nil {
return err
}
return drainAndCheck(resp)
}

func (c *Client) Heartbeat(req HeartbeatRequest) error {
resp, err := c.doRequest("POST", "/api/v2/ci/runner/heartbeat", req)
if err != nil {
return err
}
return drainAndCheck(resp)
}


func (c *Client) Disconnect() error {
resp, err := c.doRequest("POST", "/api/v2/ci/runner/disconnect", nil)
if err != nil {
return err
}
return drainAndCheck(resp)
}
Loading
Loading