Skip to content

Commit 8b87725

Browse files
authored
Merge pull request #10 from polarsignals/add-post-step
Rewrite in JS and create deployment link linking to Polar Signals Cloud
2 parents 575cf10 + cea4eb7 commit 8b87725

File tree

572 files changed

+202927
-15
lines changed

Some content is hidden

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

572 files changed

+202927
-15
lines changed

.github/workflows/github-actions-demo.yml

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
name: GitHub Actions Demo of Continuous Profiling
22
run-name: ${{ github.actor }} is testing out GitHub Actions Continuous Profiling 🚀
33
on: [push]
4+
permissions:
5+
deployments: write
6+
contents: read
7+
pull-requests: read
48
jobs:
59
Profile-Your-CI:
610
runs-on: ubuntu-latest
@@ -15,10 +19,12 @@ jobs:
1519
id: extract_branch
1620
- uses: ./ # Uses an action in the root directory.
1721
with:
18-
polarsignals_cloud_token: ${{ secrets.PSToken }}
19-
labels: 'branch=${{ steps.extract_branch.outputs.branch }};gh_run_id=${{ github.run_id }}'
22+
polarsignals_cloud_token: ${{ secrets.PSTOKEN }}
23+
labels: branch=${{ steps.extract_branch.outputs.branch }};gh_run_id=${{ github.run_id }}
24+
project_uuid: ${{ secrets.POLARSIGNALSPROJECTUUID }}
25+
github_token: ${{ secrets.GITHUB_TOKEN }}
2026
- name: Set up Go
2127
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
2228
- name: Run my little go program that does busy work.
2329
run: cd example-process && go run main.go
24-
- run: echo "🍏 This job's status is ${{ job.status }}." && sleep 300
30+
- run: echo "🍏 This job's status is ${{ job.status }}." && sleep 10

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,45 @@ If you are using Polar Signals cloud, the only thing required to configure is th
1212

1313
Profiling data from one CI run looks [like this](https://pprof.me/475d1cc/).
1414

15+
## Deployment Links
16+
17+
This action can automatically create GitHub deployments with links to your profiling data, making it easy to access the results directly from your GitHub repository. For this feature to work, the following parameters must be configured:
18+
19+
- `project_uuid`: Your Polar Signals Cloud project UUID
20+
21+
If any of the required parameters are missing, the deployment creation will be skipped without failing the workflow, and log messages will indicate the reason.
22+
23+
### Required Permissions
24+
25+
For the deployment creation to work, your workflow needs the following permissions:
26+
27+
```yaml
28+
permissions:
29+
deployments: write
30+
contents: read
31+
```
32+
33+
### Example Configuration
34+
35+
```yaml
36+
name: Profiling Workflow
37+
on: [push]
38+
39+
permissions:
40+
deployments: write
41+
contents: read
42+
43+
jobs:
44+
profile:
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v3
48+
49+
- name: Run Continuous Profiling
50+
uses: polarsignals/gh-actions-ps-profiling@main
51+
with:
52+
polarsignals_cloud_token: ${{ secrets.POLARSIGNALS_CLOUD_TOKEN }}
53+
project_uuid: 'your-project-uuid-here'
54+
labels: 'branch=${{ github.ref_name }};workflow=${{ github.workflow }}'
55+
```
56+

action.yaml renamed to action.yml

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# action.yaml
21
name: 'Polar Signals Continuous Profiling'
32
description: 'Installs Parca to continuously profile your CI/CD pipeline.'
43
inputs:
@@ -27,14 +26,21 @@ inputs:
2726
extra_args:
2827
description: 'Add any further arguments to the execution of the agent.'
2928
required: false
29+
project_uuid:
30+
description: 'Polar Signals Cloud project UUID for query URL generation.'
31+
required: false
32+
cloud_hostname:
33+
description: 'Polar Signals Cloud hostname for query URL generation.'
34+
required: false
35+
default: 'cloud.polarsignals.com'
36+
github_token:
37+
description: 'GitHub token to use for creating deployments.'
38+
required: false
39+
default: '${{ github.token }}'
40+
outputs:
41+
profiling_url:
42+
description: 'The URL to the profiling data in Polar Signals Cloud.'
3043
runs:
31-
using: 'composite'
32-
steps:
33-
- name: Install Parca Agent
34-
shell: bash
35-
run: |
36-
curl --connect-timeout 5 --max-time 60 --retry 5 --retry-delay 0 --retry-max-time 600 -sL https://github.com/parca-dev/parca-agent/releases/download/v${{ inputs.parca_agent_version }}/parca-agent_${{ inputs.parca_agent_version }}_`uname -s`_`uname -m` > parca-agent
37-
chmod +x ./parca-agent
38-
- name: Run parca agent in background
39-
shell: bash
40-
run: sudo ./parca-agent --metadata-external-labels='${{ inputs.labels }}' --profiling-duration=${{ inputs.profiling_duration }} --profiling-cpu-sampling-frequency=${{ inputs.profiling_frequency }} --node=github --remote-store-address=${{ inputs.store_address }} --remote-store-bearer-token=${{ inputs.polarsignals_cloud_token }} ${{ inputs.extra_args }} > ${{ runner.temp }}/parca-agent.log 2>&1 &
44+
using: 'node16'
45+
main: 'index.js'
46+
post: 'index.js'

example-process/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func main() {
1111
}
1212

1313
func printTenTimes() {
14-
for i := 0; i < 10; i++ {
14+
for i := 0; i < 5; i++ {
1515
fmt.Println("Looping...")
1616
doNothingButLoop()
1717
time.Sleep(5 * time.Second)

index.js

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
const core = require('@actions/core');
2+
const exec = require('@actions/exec');
3+
const tc = require('@actions/tool-cache');
4+
const os = require('os');
5+
const path = require('path');
6+
const fs = require('fs');
7+
8+
// Store timestamps file path
9+
const timestampFile = path.join(process.env.RUNNER_TEMP || '/tmp', 'parca-agent-timestamps.json');
10+
11+
// Parse labels string into an object
12+
function parseLabels(labelsString) {
13+
if (!labelsString) return {};
14+
15+
const result = {};
16+
labelsString.split(';').forEach(label => {
17+
const [key, value] = label.trim().split('=');
18+
if (key && value !== undefined) {
19+
result[key.trim()] = value.trim();
20+
}
21+
});
22+
23+
return result;
24+
}
25+
26+
async function run() {
27+
try {
28+
// Save start timestamp
29+
const startTimestamp = Date.now();
30+
31+
// Get inputs
32+
const polarsignalsCloudToken = core.getInput('polarsignals_cloud_token', { required: true });
33+
const storeAddress = core.getInput('store_address') || 'grpc.polarsignals.com:443';
34+
const parcaAgentVersion = core.getInput('parca_agent_version') || '0.38.0';
35+
const profilingFrequency = core.getInput('profiling_frequency') || '99';
36+
const profilingDuration = core.getInput('profiling_duration') || '3s';
37+
const labelsString = core.getInput('labels') || '';
38+
const extraArgs = core.getInput('extra_args') || '';
39+
const projectUuid = core.getInput('project_uuid') || '';
40+
const cloudHostname = core.getInput('cloud_hostname') || 'cloud.polarsignals.com';
41+
42+
// Parse labels
43+
const labels = parseLabels(labelsString);
44+
45+
// Save start timestamp and configuration to file
46+
fs.writeFileSync(timestampFile, JSON.stringify({
47+
startTimestamp,
48+
projectUuid,
49+
cloudHostname,
50+
labels,
51+
labelsString
52+
}));
53+
54+
core.info(`Saved start timestamp: ${startTimestamp}`);
55+
56+
// Determine platform specifics
57+
const platform = os.platform();
58+
const arch = os.arch();
59+
60+
// Map NodeJS arch to Parca arch format
61+
let parcaArch = arch;
62+
if (arch === 'x64') parcaArch = 'x86_64';
63+
if (arch === 'arm64') parcaArch = 'aarch64';
64+
65+
// Map NodeJS platform to Parca platform format
66+
let parcaPlatform = platform;
67+
if (platform === 'win32') parcaPlatform = 'Windows';
68+
if (platform === 'darwin') parcaPlatform = 'Darwin';
69+
if (platform === 'linux') parcaPlatform = 'Linux';
70+
71+
// Download URL
72+
const downloadUrl = `https://github.com/parca-dev/parca-agent/releases/download/v${parcaAgentVersion}/parca-agent_${parcaAgentVersion}_${parcaPlatform}_${parcaArch}`;
73+
74+
core.info(`Downloading Parca Agent from ${downloadUrl}...`);
75+
76+
// Download the Parca Agent
77+
const agentPath = await tc.downloadTool(downloadUrl);
78+
79+
// Make it executable
80+
await exec.exec('chmod', ['+x', agentPath]);
81+
82+
// Run the Parca agent in the background
83+
const tempLogFile = path.join(process.env.RUNNER_TEMP || '/tmp', 'parca-agent.log');
84+
85+
const args = [
86+
`--metadata-external-labels=${labelsString}`,
87+
`--profiling-duration=${profilingDuration}`,
88+
`--profiling-cpu-sampling-frequency=${profilingFrequency}`,
89+
'--node=github',
90+
`--remote-store-address=${storeAddress}`,
91+
`--remote-store-bearer-token=${polarsignalsCloudToken}`
92+
];
93+
94+
if (extraArgs) {
95+
args.push(extraArgs);
96+
}
97+
98+
core.info('Starting Parca Agent in the background...');
99+
100+
// Use spawn to run in background
101+
const { spawn } = require('child_process');
102+
const sudoPrefix = platform === 'linux' ? ['sudo'] : [];
103+
const command = sudoPrefix.concat([agentPath, ...args]);
104+
105+
const parcaProcess = spawn(command[0], command.slice(1), {
106+
detached: true,
107+
stdio: ['ignore',
108+
fs.openSync(tempLogFile, 'a'),
109+
fs.openSync(tempLogFile, 'a')
110+
]
111+
});
112+
113+
// Detach the process
114+
parcaProcess.unref();
115+
116+
core.info(`Parca Agent started with PID ${parcaProcess.pid}`);
117+
core.info(`Logs available at: ${tempLogFile}`);
118+
119+
} catch (error) {
120+
core.setFailed(`Action failed with error: ${error.message}`);
121+
}
122+
}
123+
124+
async function post() {
125+
try {
126+
// Get ending timestamp
127+
const endTimestamp = Date.now();
128+
129+
// Read start timestamp from file
130+
if (!fs.existsSync(timestampFile)) {
131+
core.warning('Start timestamp file not found. Cannot create Polar Signals Cloud query.');
132+
return;
133+
}
134+
135+
const data = JSON.parse(fs.readFileSync(timestampFile, 'utf8'));
136+
const { startTimestamp, projectUuid, cloudHostname, labels, labelsString } = data;
137+
138+
// Build the URL with the proper format
139+
let queryUrl = `https://${cloudHostname}/`;
140+
141+
// Append project UUID if provided
142+
if (projectUuid) {
143+
queryUrl += `projects/${projectUuid}?`;
144+
}
145+
146+
// Build label selector string
147+
let labelSelector = '';
148+
if (labelsString && Object.keys(labels).length > 0) {
149+
const labelSelectors = [];
150+
151+
for (const [key, value] of Object.entries(labels)) {
152+
labelSelectors.push(`${key}="${value}"`);
153+
}
154+
155+
if (labelSelectors.length > 0) {
156+
labelSelector = '{' + labelSelectors.join(',') + '}';
157+
}
158+
}
159+
160+
// Define parameters for the URL
161+
const baseMetric = 'parca_agent:samples:count:cpu:nanoseconds:delta';
162+
const expression = labelSelector ? `${baseMetric}${labelSelector}` : baseMetric;
163+
const encodedExpression = encodeURIComponent(expression);
164+
165+
// Calculate a reasonable step count
166+
const durationSeconds = Math.floor((endTimestamp - startTimestamp) / 1000);
167+
const stepCount = Math.min(Math.max(Math.floor(durationSeconds / 10), 50), 500); // Between 50 and 500 steps
168+
169+
// Add the query parameters with the complex format
170+
queryUrl += `query_browser_mode=simple`;
171+
queryUrl += `&step_count=${stepCount}`;
172+
queryUrl += `&expression_a=${encodedExpression}`;
173+
queryUrl += `&from_a=${startTimestamp}`;
174+
queryUrl += `&to_a=${endTimestamp}`;
175+
queryUrl += `&time_selection_a=custom`;
176+
queryUrl += `&sum_by_a=comm`;
177+
queryUrl += `&merge_from_a=${startTimestamp}`;
178+
queryUrl += `&merge_to_a=${endTimestamp}`;
179+
queryUrl += `&selection_a=${encodedExpression}`;
180+
181+
core.info('Polar Signals Cloud Query Information:');
182+
core.info(`- Start time: ${new Date(startTimestamp).toISOString()} (${startTimestamp}ms)`);
183+
core.info(`- End time: ${new Date(endTimestamp).toISOString()} (${endTimestamp}ms)`);
184+
core.info(`- Duration: ${Math.round((endTimestamp - startTimestamp) / 1000)} seconds`);
185+
core.info(`- Query URL: ${queryUrl}`);
186+
187+
// Set output for the action
188+
core.setOutput('profiling_url', queryUrl);
189+
190+
// Create a GitHub deployment if running in GitHub Actions and all required parameters are available
191+
if (process.env.GITHUB_ACTIONS) {
192+
try {
193+
const github_token = core.getInput('github_token');
194+
const repository = process.env.GITHUB_REPOSITORY;
195+
const [owner, repo] = (repository || '').split('/');
196+
const ref = process.env.GITHUB_REF || process.env.GITHUB_SHA;
197+
198+
// Check if all required parameters are available for deployment
199+
if (github_token && owner && repo && ref && projectUuid && queryUrl) {
200+
core.info(`Creating deployment for ${owner}/${repo} at ${ref}`);
201+
202+
const octokit = require('@octokit/rest');
203+
const { Octokit } = octokit;
204+
const client = new Octokit({
205+
auth: github_token
206+
});
207+
208+
const deployment = await client.repos.createDeployment({
209+
owner,
210+
repo,
211+
ref,
212+
environment: 'polar-signals-cloud',
213+
required_contexts: [],
214+
auto_merge: false,
215+
description: 'Polar Signals Profiling Results',
216+
transient_environment: true,
217+
production_environment: false
218+
});
219+
220+
// Create a deployment status
221+
if (deployment.data.id) {
222+
const deploymentId = deployment.data.id;
223+
await client.repos.createDeploymentStatus({
224+
owner,
225+
repo,
226+
deployment_id: deploymentId,
227+
state: 'success',
228+
description: 'Profiling data is available',
229+
environment_url: queryUrl,
230+
log_url: queryUrl,
231+
auto_inactive: true
232+
});
233+
234+
core.info(`Created deployment with ID: ${deploymentId}`);
235+
}
236+
} else {
237+
core.info('Skipping GitHub deployment creation due to missing required parameters:');
238+
if (!github_token) core.info('- Missing github_token');
239+
if (!owner || !repo) core.info(`- Missing repository information: ${repository}`);
240+
if (!ref) core.info('- Missing ref information');
241+
if (!projectUuid) core.info('- Missing project_uuid');
242+
if (!queryUrl) core.info('- Missing queryUrl');
243+
}
244+
} catch (deployError) {
245+
core.warning(`Failed to create GitHub deployment: ${deployError.message}`);
246+
}
247+
}
248+
249+
// Clean up timestamp file
250+
fs.unlinkSync(timestampFile);
251+
252+
} catch (error) {
253+
core.warning(`Post action failed with error: ${error.message}`);
254+
}
255+
}
256+
257+
// Determine whether to run the main action or post action
258+
const isPost = !!process.env.STATE_isPost;
259+
if (isPost) {
260+
post();
261+
} else {
262+
run();
263+
// Save state to indicate post action should run
264+
core.saveState('isPost', 'true');
265+
}

node_modules/.bin/semver

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)