Skip to content

Commit ac33bfd

Browse files
initial implementation of trigger sentinel
1 parent 3851037 commit ac33bfd

File tree

255 files changed

+37480
-0
lines changed

Some content is hidden

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

255 files changed

+37480
-0
lines changed

.github/workflows/test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
on:
3+
pull_request:
4+
push:
5+
branches: [main]
6+
jobs:
7+
test:
8+
name: Test
9+
runs-on: ubuntu-latest
10+
permissions:
11+
id-token: write
12+
steps:
13+
- name: Setup Node.js
14+
uses: actions/setup-node@v3
15+
with:
16+
node-version: "16"
17+
- name: Checkout code
18+
uses: actions/checkout@v3
19+
# - name: setup plural
20+
# id: plural
21+
# uses: pluralsh/setup-plural@v1
22+
# with:
23+
24+
# consoleUrl: https://console.plrldemo.onplural.sh
25+
# - name: Test trigger pipeline
26+
# uses: ./
27+
# with:
28+
# url: https://console.plrldemo.onplural.sh
29+
# token: ${{ steps.plural.outputs.consoleToken }}
30+
# pipeline: plrl-console-test
31+
# context: |
32+
# {
33+
# "flow": "test-flow",
34+
# "tag": "0.1.2"
35+
# }

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# trigger-sentinel Github Action
2+
3+
Github Action to invoke a Plural Sentinel. Sentinels are meant to be reusable integration tests that are invocable via API in a number of different integration points. In this case, if you need to add a multi-cluster or deep infrastructure probe to your Github Actions pipeline, `trigger-sentinel` is likely a good fit.
4+
5+
## Inputs
6+
7+
```yaml
8+
url:
9+
description: the url of your Plural Console instance
10+
required: true
11+
token:
12+
description: the token to use to authenticate with Plural Console
13+
required: true
14+
sentinel:
15+
description: the name of the sentinel to trigger
16+
required: true
17+
wait:
18+
description: whether to wait on the sentinel to finish
19+
required: false
20+
```
21+
22+
## Example Usage
23+
24+
```yaml
25+
- name: Authenticate
26+
id: plural
27+
uses: pluralsh/setup-plural@v2
28+
with:
29+
consoleUrl: https://my.console.cloud.plural.sh
30+
email: [email protected] # the email bound to your OIDC federated credential
31+
- name: Trigger PR
32+
uses: pluralsh/trigger-sentinel@v1
33+
with:
34+
url: https://my.console.cloud.plural.sh
35+
token: ${{ steps.plural.outputs.consoleToken }}
36+
sentinel: test-sentinel
37+
wait: 'true'
38+
```
39+
40+
For this to be possible you need to have configured the following:
41+
42+
1. Federated credential to allow `[email protected]` to exchange a GH actions token for a temporary Plural token. This token should have at least the scope `createPipelineContext`.
43+
2. A write binding on the `test-sentinel` Sentinel to allow `[email protected]` to invoke it. This is not permissible by default unless that user is an admin.
44+
3. The `test-sentinel` sentinel itself. You can learn more at https://docs.plural.sh/plural-features/plural-ai/sentinels
45+

action.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Trigger Sentinel
2+
description: Trigger a Plural Sentinel and optionally wait for it to finish
3+
4+
inputs:
5+
url:
6+
description: the url of your Plural Console instance
7+
required: true
8+
token:
9+
description: the token to use to authenticate with Plural Console
10+
required: true
11+
sentinel:
12+
description: the name of the sentinel to trigger
13+
required: true
14+
wait:
15+
description: whether to wait for the sentinel to finish
16+
required: false
17+
default: 'false'
18+
waitDurationSeconds:
19+
description: the maximum duration to wait for the sentinel to finish
20+
required: false
21+
default: '600'
22+
runs:
23+
using: node20
24+
main: index.js

index.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as core from "@actions/core";
2+
import * as path from "path";
3+
4+
const runDoc = `
5+
mutation RunSentinel($name: String!) {
6+
runSentinel(name: $name) {
7+
id
8+
status
9+
sentinel { id }
10+
}
11+
}
12+
`
13+
14+
const fetchDoc = `
15+
query GetSentinelRun($id: ID!) {
16+
sentinelRun(id: $id) {
17+
id
18+
status
19+
}
20+
}
21+
`
22+
23+
async function main() {
24+
const url = core.getInput('url');
25+
const token = core.getInput('token');
26+
const sentinel = core.getInput('sentinel');
27+
const wait = core.getInput('wait') === 'true';
28+
29+
const response = await fetch(path.join(url, 'gql'), {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/json',
33+
'Authorization': `Bearer ${token}`,
34+
},
35+
body: JSON.stringify({
36+
query: runDoc,
37+
variables: { name: sentinel }
38+
}),
39+
});
40+
41+
if (!response.ok) {
42+
const body = await response.text();
43+
core.setFailed(`Failed to create pull request: ${response.status}\n${body}`);
44+
return;
45+
}
46+
47+
const resp = await response.json();
48+
const run = resp.data?.runSentinel;
49+
50+
if (!run) {
51+
core.setFailed(`Failed to run sentinel: ${JSON.stringify(resp.errors)}`);
52+
return;
53+
}
54+
core.info(`Ran sentinel: ${run.id}. View the sentinel at ${path.join(url, "ai", 'sentinels', run.sentinel.id, "runs", run.id)}`);
55+
56+
if (wait) {
57+
var maxDuration = 10 * 60 * 1000;
58+
var parsed = parseInt(core.getInput('waitDurationSeconds'))
59+
if (parsed) {
60+
if (parsed < 0) {
61+
core.setFailed(`waitDurationSeconds must be a positive number, got: ${parsed}`);
62+
return;
63+
}
64+
maxDuration = parsed * 1000;
65+
} else {
66+
core.info(`Using default wait duration of 10 minutes`);
67+
}
68+
69+
await pollSentinelRun(token, url, run.id, Date.now(), maxDuration);
70+
}
71+
}
72+
73+
async function pollSentinelRun(token, url, id, startTime, maxDuration=10 * 60 * 1000) {
74+
try {
75+
if (Date.now() - startTime > maxDuration) {
76+
core.setFailed(`Sentinel run ${id} timed out after ${maxDuration / 1000} seconds`);
77+
return;
78+
}
79+
80+
core.info(`Polling sentinel run ${id}...`);
81+
82+
const response = await fetch(path.join(url, 'gql'), {
83+
method: 'POST',
84+
headers: {
85+
'Content-Type': 'application/json',
86+
'Authorization': `Bearer ${token}`,
87+
},
88+
body: JSON.stringify({ query: fetchDoc, variables: { id } }),
89+
});
90+
91+
const resp = await response.json();
92+
const run = resp.data?.sentinelRun;
93+
94+
if (!run) {
95+
core.setFailed(`Failed to fetch sentinel run: ${JSON.stringify(resp.errors)}`);
96+
return;
97+
}
98+
99+
if (run.status === 'SUCCESS') {
100+
core.info(`Sentinel run ${id} finished successfully`);
101+
return;
102+
}
103+
104+
if (run.status === 'FAILED') {
105+
core.setFailed(`Sentinel run ${id} failed`);
106+
return;
107+
}
108+
} catch (error) {
109+
core.info(`Failed to poll sentinel run: ${error}`);
110+
}
111+
112+
setTimeout(() => pollSentinelRun(id, startTime, maxDuration), 2000); // poll every 2 seconds
113+
}
114+
115+
main();

node_modules/.package-lock.json

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

node_modules/@actions/core/LICENSE.md

Lines changed: 9 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)