Skip to content

Commit 335a9b2

Browse files
committed
Merge branch 'PLEX-1920' into solana-logtrigger-e2e
2 parents d35ecf1 + 977d376 commit 335a9b2

File tree

86 files changed

+5370
-675
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5370
-675
lines changed

.github/workflows/build-publish.yml

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ jobs:
1717
is-release: ${{ steps.release-tag-check.outputs.is-release }}
1818
is-pre-release: ${{ steps.release-tag-check.outputs.is-pre-release }}
1919
ccip-image-tag: ${{ steps.compute-ccip-tag.outputs.ccip-image-tag }}
20+
prerelease-phase: ${{ steps.detect-prerelease-phase.outputs.prerelease-phase }}
21+
is-hotfix: ${{ steps.detect-hotfix.outputs.is-hotfix }}
2022
steps:
21-
- name: Checkout repository
22-
uses: actions/checkout@v4
23+
- uses: actions/checkout@v4
2324
with:
2425
persist-credentials: false
26+
2527
- name: Check release tag
2628
id: release-tag-check
2729
uses: smartcontractkit/.github/actions/release-tag-check@c5c4a8186da4218cff6cac8184e47dd3dec69ba3 # release-tag-check@0.1.0
30+
2831
- name: Compute CCIP image tag
2932
id: compute-ccip-tag
3033
shell: bash
@@ -38,6 +41,47 @@ jobs:
3841
tag_without_v="${GIT_TAG#v}"
3942
ccip_tag=$(echo "$tag_without_v" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)-(.*)$/\1-ccip-\2/')
4043
echo "ccip-image-tag=$ccip_tag" | tee -a "$GITHUB_OUTPUT"
44+
45+
- name: Detect prerelease phase
46+
id: detect-prerelease-phase
47+
if: ${{ steps.release-tag-check.outputs.is-pre-release == 'true' && (contains(github.ref_name, '-beta.') || contains(github.ref_name, '-rc.')) }}
48+
shell: bash
49+
env:
50+
GIT_TAG: ${{ github.ref_name }}
51+
run: |
52+
phase=""
53+
54+
if [[ "$GIT_TAG" =~ -beta\.[0-9]+$ ]]; then
55+
phase="beta"
56+
elif [[ "$GIT_TAG" =~ -rc\.[0-9]+$ ]]; then
57+
# Release candidates.
58+
phase="rc"
59+
else
60+
echo "::error::Unable to determine prerelease phase from tag: $GIT_TAG"
61+
exit 1
62+
fi
63+
echo "prerelease-phase=$phase" | tee -a "$GITHUB_OUTPUT"
64+
65+
- name: Detect hotfix release
66+
id: detect-hotfix
67+
shell: bash
68+
env:
69+
GIT_TAG: ${{ github.ref_name }}
70+
run: |
71+
# Extract patch version from tag (e.g., v2.33.1-beta.0 -> 1)
72+
# Hotfix releases have non-zero patch versions
73+
if [[ "$GIT_TAG" =~ ^v[0-9]+\.[0-9]+\.([0-9]+) ]]; then
74+
patch_version="${BASH_REMATCH[1]}"
75+
if [[ "$patch_version" -gt 0 ]]; then
76+
echo "is-hotfix=true" | tee -a "$GITHUB_OUTPUT"
77+
else
78+
echo "is-hotfix=false" | tee -a "$GITHUB_OUTPUT"
79+
fi
80+
else
81+
echo "is-hotfix=false" | tee -a "$GITHUB_OUTPUT"
82+
echo "Unable to extract patch version, assuming not a hotfix"
83+
fi
84+
4185
- name: Check for VERSION file bump on tags
4286
if: ${{ github.repository == 'smartcontractkit/chainlink' }}
4387
uses: ./.github/actions/version-file-bump
@@ -66,6 +110,7 @@ jobs:
66110
docker-cache-behaviour: "disable"
67111
docker-manifest-sign: true
68112
docker-registry-url-override: public.ecr.aws/chainlink
113+
github-runner-arm64: ${{ github.repository != 'smartcontractkit/chainlink' && 'ubuntu-24.04-4cores-16GB-ARM' || '' }}
69114
docker-image-tag-strip-prefix: v # strip out the "v" prefix from the git tag for the image tag.
70115
git-sha: ${{ github.sha }}
71116
github-event-name: ${{ github.event_name }}
@@ -103,6 +148,7 @@ jobs:
103148
docker-cache-behaviour: "disable"
104149
docker-manifest-sign: true
105150
docker-registry-url-override: public.ecr.aws/chainlink
151+
github-runner-arm64: ${{ github.repository != 'smartcontractkit/chainlink' && 'ubuntu-24.04-4cores-16GB-ARM' || '' }}
106152
docker-image-tag-override: ${{ needs.checks.outputs.ccip-image-tag }}
107153
git-sha: ${{ github.sha }}
108154
github-event-name: ${{ github.event_name }}
@@ -115,6 +161,50 @@ jobs:
115161
AWS_ROLE_GATI_ARN: ${{ secrets.AWS_OIDC_CHAINLINK_READ_ONLY_TOKEN_ISSUER_ROLE_ARN }}
116162
AWS_LAMBDA_GATI_URL: ${{ secrets.AWS_INFRA_RELENG_TOKEN_ISSUER_LAMBDA_URL }}
117163

164+
deploy:
165+
name: "Deploy"
166+
needs: [checks, docker-ccip]
167+
# We are only deploying CCIP pre-releases and skipping hotfix deployments for now.
168+
if: needs.checks.outputs.is-pre-release == 'true' && needs.checks.outputs.is-hotfix == 'false'
169+
runs-on: ubuntu-latest
170+
permissions:
171+
contents: read
172+
id-token: write
173+
steps:
174+
- uses: actions/checkout@v6
175+
with:
176+
persist-credentials: false
177+
178+
- name: Deploy CCIP Beta
179+
if: needs.checks.outputs.prerelease-phase == 'beta'
180+
uses: ./.github/actions/deploy-image
181+
with:
182+
aws-role-arn: ${{ secrets.AWS_RELENG_PROD_GATI_WORKFLOW_INVOKE_ARN }}
183+
aws-lambda-url: ${{ secrets.AWS_INFRA_RELENG_TOKEN_ISSUER_LAMBDA_URL }}
184+
aws-region: ${{ secrets.AWS_REGION }}
185+
repo-destination: ${{ secrets.REPO_K8S_DEPLOY }}
186+
oci-image-tag: ${{ needs.docker-ccip.outputs.docker-manifest-tag }}
187+
oci-repository-url: public.ecr.aws/chainlink/ccip
188+
pr-close-enabled: false
189+
pr-labels: preview-stage,force-preview
190+
products: |
191+
ccip-prereleases-staging
192+
193+
- name: Deploy CCIP RC
194+
if: needs.checks.outputs.prerelease-phase == 'rc'
195+
uses: ./.github/actions/deploy-image
196+
with:
197+
aws-role-arn: ${{ secrets.AWS_RELENG_PROD_GATI_WORKFLOW_INVOKE_ARN }}
198+
aws-lambda-url: ${{ secrets.AWS_INFRA_RELENG_TOKEN_ISSUER_LAMBDA_URL }}
199+
aws-region: ${{ secrets.AWS_REGION }}
200+
repo-destination: ${{ secrets.REPO_K8S_DEPLOY }}
201+
oci-image-tag: ${{ needs.docker-ccip.outputs.docker-manifest-tag }}
202+
oci-repository-url: public.ecr.aws/chainlink/ccip
203+
pr-close-enabled: false
204+
pr-labels: preview-stage,force-preview
205+
products: |
206+
ccip-prereleases-prod-testnet
207+
118208
# Notify Slack channel for new git tags associated with pre-releases.
119209
# Final release notifications originate from the release coordinator repo.
120210
slack-notify-core:

core/capabilities/fakes/confidential_http_action.go

Lines changed: 64 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"errors"
7+
"fmt"
78
"io"
89
"net/http"
910
"strings"
@@ -50,110 +51,93 @@ func NewDirectConfidentialHTTPAction(lggr logger.Logger) *DirectConfidentialHTTP
5051
return fc
5152
}
5253

53-
func (fh *DirectConfidentialHTTPAction) SendRequests(ctx context.Context, metadata commonCap.RequestMetadata, input *confidentialhttp.EnclaveActionInput) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPEnclaveResponseData], caperrors.Error) {
54-
fh.eng.Infow("Confidential HTTP Action SendRequests Started", "input", input, "secretsCount", len(input.GetVaultDonSecrets()))
54+
func (fh *DirectConfidentialHTTPAction) SendRequest(ctx context.Context, metadata commonCap.RequestMetadata, input *confidentialhttp.ConfidentialHTTPRequest) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse], caperrors.Error) {
55+
fh.eng.Infow("Confidential HTTP Action SendRequest Started", "input", input, "secretsCount", len(input.GetVaultDonSecrets()))
5556

5657
// Warn if secrets are provided - this fake does not handle secret resolution
5758
if len(input.GetVaultDonSecrets()) > 0 {
5859
fh.eng.Warnw("This fake does not handle secrets - VaultDonSecrets will be ignored. Template variables like {{.secretName}} will not be resolved.", "secretsCount", len(input.GetVaultDonSecrets()))
5960
}
6061

61-
if input.GetInput() == nil {
62-
return nil, caperrors.NewPublicUserError(errors.New("input cannot be nil"), caperrors.InvalidArgument)
62+
req := input.GetRequest()
63+
if req == nil {
64+
return nil, caperrors.NewPublicUserError(errors.New("request cannot be nil"), caperrors.InvalidArgument)
6365
}
6466

65-
requests := input.GetInput().GetRequests()
66-
if len(requests) == 0 {
67-
return nil, caperrors.NewPublicUserError(errors.New("no requests provided"), caperrors.InvalidArgument)
68-
}
69-
70-
// Process each request
71-
responses := make([]*confidentialhttp.ResponseTemplate, 0, len(requests))
72-
73-
for i, req := range requests {
74-
fh.eng.Infow("Processing confidential HTTP request", "index", i, "url", req.GetUrl(), "method", req.GetMethod())
67+
fh.eng.Infow("Processing confidential HTTP request", "url", req.GetUrl(), "method", req.GetMethod())
7568

76-
// Create HTTP client with timeout (default 30 seconds)
77-
timeout := time.Duration(30) * time.Second
78-
client := &http.Client{
79-
Timeout: timeout,
80-
}
69+
// Create HTTP client with timeout (default 30 seconds)
70+
timeout := time.Duration(30) * time.Second
71+
client := &http.Client{
72+
Timeout: timeout,
73+
}
8174

82-
// Validate HTTP method
83-
method := strings.TrimSpace(req.GetMethod())
84-
if method == "" {
85-
return nil, caperrors.NewPublicUserError(errors.New("http method cannot be empty"), caperrors.InvalidArgument)
86-
}
87-
method = strings.ToUpper(method)
75+
// Validate HTTP method
76+
method := strings.TrimSpace(req.GetMethod())
77+
if method == "" {
78+
return nil, caperrors.NewPublicUserError(errors.New("http method cannot be empty"), caperrors.InvalidArgument)
79+
}
80+
method = strings.ToUpper(method)
81+
82+
// Create request body
83+
var body io.Reader
84+
if bodyStr := req.GetBodyString(); bodyStr != "" {
85+
body = bytes.NewReader([]byte(bodyStr))
86+
} else if bodyBytes := req.GetBodyBytes(); len(bodyBytes) > 0 {
87+
body = bytes.NewReader(bodyBytes)
88+
}
8889

89-
// Create request body
90-
var body io.Reader
91-
if len(req.GetBody()) > 0 {
92-
body = bytes.NewReader([]byte(req.GetBody()))
93-
}
90+
// Create the HTTP request
91+
httpReq, err := http.NewRequestWithContext(ctx, method, req.GetUrl(), body)
92+
if err != nil {
93+
fh.eng.Errorw("Failed to create HTTP request", "error", err)
94+
return nil, caperrors.NewPublicUserError(fmt.Errorf("failed to create HTTP request: %w", err), caperrors.InvalidArgument)
95+
}
9496

95-
// Create the HTTP request
96-
httpReq, err := http.NewRequestWithContext(ctx, method, req.GetUrl(), body)
97-
if err != nil {
98-
fh.eng.Errorw("Failed to create HTTP request", "error", err, "index", i)
99-
responses = append(responses, &confidentialhttp.ResponseTemplate{
100-
StatusCode: 0,
101-
Body: []byte(err.Error()),
102-
})
103-
continue
104-
}
97+
// Add headers
98+
for name, value := range req.GetHeaders() {
99+
httpReq.Header.Set(name, value)
100+
}
105101

106-
// Add headers
107-
for _, header := range req.GetHeaders() {
108-
parts := strings.SplitN(header, ":", 2)
109-
if len(parts) == 2 {
110-
httpReq.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
111-
}
112-
}
102+
// Make the HTTP request
103+
resp, err := client.Do(httpReq)
104+
if err != nil {
105+
fh.eng.Errorw("Failed to execute confidential HTTP request", "error", err)
106+
return nil, caperrors.NewPublicUserError(fmt.Errorf("failed to execute HTTP request: %w", err), caperrors.InvalidArgument)
107+
}
108+
defer resp.Body.Close()
113109

114-
// Make the HTTP request
115-
resp, err := client.Do(httpReq)
116-
if err != nil {
117-
fh.eng.Errorw("Failed to execute confidential HTTP request", "error", err, "index", i)
118-
responses = append(responses, &confidentialhttp.ResponseTemplate{
119-
StatusCode: 0,
120-
Body: []byte(err.Error()),
121-
})
122-
continue
123-
}
110+
// Read response body
111+
respBody, err := io.ReadAll(resp.Body)
112+
if err != nil {
113+
fh.eng.Errorw("Failed to read response body", "error", err)
114+
return nil, caperrors.NewPublicUserError(fmt.Errorf("failed to read response body: %w", err), caperrors.InvalidArgument)
115+
}
124116

125-
// Read response body
126-
respBody, err := io.ReadAll(resp.Body)
127-
resp.Body.Close()
128-
if err != nil {
129-
fh.eng.Errorw("Failed to read response body", "error", err, "index", i)
130-
responses = append(responses, &confidentialhttp.ResponseTemplate{
131-
StatusCode: int64(resp.StatusCode),
132-
Body: []byte(err.Error()),
117+
// Convert response headers to []*Header
118+
var responseHeaders []*confidentialhttp.Header
119+
for name, values := range resp.Header {
120+
for _, value := range values {
121+
responseHeaders = append(responseHeaders, &confidentialhttp.Header{
122+
Name: name,
123+
Value: value,
133124
})
134-
continue
135-
}
136-
137-
// Create response template
138-
response := &confidentialhttp.ResponseTemplate{
139-
StatusCode: int64(resp.StatusCode),
140-
Body: respBody,
141125
}
142-
responses = append(responses, response)
143-
fh.eng.Infow("Confidential HTTP request finished", "index", i, "status", resp.StatusCode, "url", req.GetUrl())
144126
}
145127

146-
// Create response data
147-
responseData := &confidentialhttp.HTTPEnclaveResponseData{
148-
Responses: responses,
128+
// Create response
129+
response := &confidentialhttp.HTTPResponse{
130+
StatusCode: uint32(resp.StatusCode), //nolint:gosec // HTTP status codes are always positive (100-599)
131+
Body: respBody,
132+
Headers: responseHeaders,
149133
}
150134

151-
responseAndMetadata := commonCap.ResponseAndMetadata[*confidentialhttp.HTTPEnclaveResponseData]{
152-
Response: responseData,
135+
responseAndMetadata := commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse]{
136+
Response: response,
153137
ResponseMetadata: commonCap.ResponseMetadata{},
154138
}
155139

156-
fh.eng.Infow("Confidential HTTP Action Finished", "requestCount", len(requests), "responseCount", len(responses))
140+
fh.eng.Infow("Confidential HTTP Action Finished", "status", resp.StatusCode, "url", req.GetUrl())
157141
return &responseAndMetadata, nil
158142
}
159143

core/cmd/shell_local.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838

3939
"github.com/smartcontractkit/chainlink/v2/core/build"
4040
"github.com/smartcontractkit/chainlink/v2/core/logger"
41+
beholderServices "github.com/smartcontractkit/chainlink/v2/core/services/beholder"
4142
"github.com/smartcontractkit/chainlink/v2/core/services/keystore"
4243
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype"
4344
"github.com/smartcontractkit/chainlink/v2/core/services/pg"
@@ -322,6 +323,12 @@ func (s *Shell) runNode(c *cli.Context) error {
322323
lggr := logger.Sugared(s.Logger.Named("RunNode"))
323324
lggr.Infow("configuration args", "config files", s.configFiles, "secret files", s.secretsFiles)
324325

326+
beholderConfigRecorder := beholderServices.NewConfigRecorder(lggr, 1*time.Hour)
327+
if err := beholderConfigRecorder.Start(ctx); err != nil {
328+
return fmt.Errorf("failed to start beholder config recorder service: %w", err)
329+
}
330+
defer beholderConfigRecorder.Close()
331+
325332
s.Config.LogConfiguration(lggr.Debugf, lggr.Warnf)
326333

327334
if err := s.Config.Validate(); err != nil {

core/scripts/cre/environment/README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,13 @@ Refer to [this document](https://docs.google.com/document/d/1HtVLv2ipx2jvU15WYOi
167167
Environment can be setup by running `go run . env setup` inside `core/scripts/cre/environment` folder. Its configuration is defined in [configs/setup.toml](configs/setup.toml) file. It will make sure that:
168168
- you have AWS CLI installed and configured
169169
- you have GH CLI installed and authenticated
170-
- you have required Job Distributor and Chip Ingress (Beholder) images
170+
- you have required Job Distributor, Chip Ingress, and Chip Config images
171171
- install and copy all capability binaries to expected location
172172

173+
**Image Versioning:**
174+
175+
Docker images for Beholder services (chip-ingress, chip-config) use commit-based tags instead of mutable tags like `local-cre`. This ensures you always know which version is running and prevents hard-to-debug issues from version mismatches. The exact versions are defined in [configs/setup.toml](configs/setup.toml).
176+
173177
Capability installation is two fold. Private and local plugins are compiled locally and then copied to the running Docker container. Public plugins are installed, when the Docker image is built. The reason is that capability developers need a way to quickly test capabilities they are working on, without having to push the code to remote repository, so that it could be installed in the Docker image (and that's because local capability code is usually located outside Docker build context and thus unavailable).
174178

175179
Private capabilities are defined in [plugins.private.yaml](../../../../plugins/plugins.private.yaml) file, public in [plugins.public.yaml](../../../../plugins/plugins.public.yaml). Local ones include:
@@ -238,12 +242,28 @@ Once up and running you will be able to access [CRE topic view](http://localhost
238242
#### Filtering out heartbeats
239243
Heartbeat messages spam the topic, so it's highly recommended that you add a JavaScript filter that will exclude them using the following code: `return value.msg !== 'heartbeat';`.
240244

241-
If environment is aready running you can start just the Beholder stack (and register protos) with:
245+
If environment is already running you can start just the Beholder stack (and register protos) with:
242246
```bash
243247
go run . env beholder start
244248
```
245249

246-
> This assumes you have `chip-ingress:bbac3c825b061546980fa9d7dc0f3e8c34347bcf` Docker image on your local machine. Without it Beholder won't be able to start. If you do not, close the [Atlas](https://github.com/smartcontractkit/atlas) repository, and then in `atlas/chip-ingress` run `docker build -t chip-ingress:bbac3c825b061546980fa9d7dc0f3e8c34347bcf .`
250+
**Image Requirements:**
251+
252+
Beholder requires `chip-ingress` and `chip-config` Docker images with specific versions defined in [configs/setup.toml](configs/setup.toml). The image tags use commit hashes for version tracking (e.g., `chip-ingress:da84cb72d3a160e02896247d46ab4b9806ebee2f`).
253+
254+
When starting Beholder, the system will:
255+
- **In CI (`CI=true`)**: Skip image checks (docker-compose will pull at runtime)
256+
- **Interactive terminal**: Auto-build missing images from sources. If build fails and `AWS_ECR` is set, you'll be offered to pull from ECR instead
257+
- **Non-interactive (tests, scripts)**: Auto-pull from ECR if `AWS_ECR` is set, otherwise fail with instructions
258+
259+
To manually ensure images are available, run:
260+
```bash
261+
# Build from sources
262+
go run . env setup
263+
264+
# Or pull from ECR (requires AWS SSO access)
265+
AWS_ECR=<account-id>.dkr.ecr.us-west-2.amazonaws.com go run . env setup
266+
```
247267

248268
### Storage
249269

0 commit comments

Comments
 (0)