Skip to content

Commit 2c842d9

Browse files
eloymgzerokguicaulada
authored
feat(create-github-app-token): added retries and test workflow (#1294)
* refactor of the actions adding retries and multi app * prettier format * added mask * token mask * precommit exclude * more debug logs * better github token creation logs * Added CODEOWNERS and fix GitHub name Co-authored-by: Horst Gutmann <[email protected]> Co-authored-by: Guilherme Caulada <[email protected]> --------- Co-authored-by: Horst Gutmann <[email protected]> Co-authored-by: Guilherme Caulada <[email protected]>
1 parent 78d8256 commit 2c842d9

File tree

7 files changed

+185
-19
lines changed

7 files changed

+185
-19
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Test Create GitHub App Token
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- "actions/create-github-app-token/**"
9+
- ".github/workflows/test-create-github-app-token.yaml"
10+
11+
pull_request:
12+
paths:
13+
- "actions/create-github-app-token/**"
14+
- ".github/workflows/test-create-github-app-token.yaml"
15+
types:
16+
- edited
17+
- opened
18+
- ready_for_review
19+
- synchronize
20+
21+
merge_group:
22+
23+
permissions:
24+
contents: read
25+
id-token: write # required for OIDC → Vault
26+
27+
jobs:
28+
test:
29+
runs-on: ubuntu-latest
30+
31+
# The `get-vault-secrets` action only works when run from a `grafana`
32+
# repository, so skip this test if the PR is from a different repository. We
33+
# will still get a run of this workflow for the change before merging, as we
34+
# use merge queues.
35+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == 'grafana'
36+
37+
strategy:
38+
matrix:
39+
instance:
40+
- dev
41+
- ops
42+
- invalid
43+
44+
steps:
45+
- name: Checkout
46+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
47+
with:
48+
persist-credentials: false
49+
50+
- name: Run Create GitHub App Token action
51+
id: app-token
52+
uses: ./actions/create-github-app-token
53+
with:
54+
github_app: grafana-create-app-token-test
55+
vault_instance: ${{ matrix.instance }}
56+
continue-on-error: ${{ matrix.instance == 'invalid' }}
57+
58+
- name: Validate token by calling GitHub API
59+
if: ${{ matrix.instance != 'invalid' }}
60+
env:
61+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
62+
run: |
63+
echo "Testing token for instance: ${{ matrix.instance }}"
64+
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
65+
-H "Accept: application/vnd.github+json" \
66+
https://api.github.com/repos/${{ github.repository }} \
67+
| jq '.full_name'
68+
69+
- name: Skip invalid instance
70+
if: ${{ matrix.instance == 'invalid' }}
71+
run: echo "Invalid vault_instance test run is expected to fail and is allowed."

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ repos:
1818
- eslint
1919
entry: oven/bun:1.2.17@sha256:2cd6a1d9e3d505725243c9564cca08465fc6ffb12c065a09261992e650995ee6
2020
files: \.[jt]s$
21+
exclude: "create_github_token.js"
2122
language: docker_image
2223

2324
- id: prettier

CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@
1616

1717
# Observability
1818
/actions/azure-trusted-signing/ @grafana/o11y-dept
19+
20+
# Platform Infrasec
21+
/actions/create-github-app-token @grafana/platform-infrasec

actions/create-github-app-token/action.yaml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ inputs:
66
default: default
77
github_app:
88
description: |
9-
GitHub app name in Vault
9+
GitHub app name in Vault. You can define mutiple app to do a loadbalancing in a comma separated format.
1010
vault_instance:
1111
description: |
1212
The Vault instance to use (`dev` or `ops`). Defaults to `ops`.
@@ -36,25 +36,27 @@ runs:
3636
REF_SHA=$(echo -n "$RAW_NAME" | sed -E 's|^[^/]*/[^/]*/||' | sed -E 's/@.*//' | sha256sum | awk '{print $1}')
3737
echo "ref_sha=$REF_SHA" >> "$GITHUB_OUTPUT"
3838
39-
- id: get-github-jwt-token
39+
- id: get-github-jwt-token-proxy
4040
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
4141
env:
4242
VAULT_INSTANCE: ${{ inputs.vault_instance }}
4343
with:
4444
script: |
45-
const jwt = await core.getIDToken("vault-github-actions-grafana-"+process.env.VAULT_INSTANCE);
46-
core.setSecret(jwt);
47-
core.setOutput("github-jwt",jwt);
45+
const audience = "vault-github-actions-grafana-" + process.env.VAULT_INSTANCE
46+
const script = require(process.env.GITHUB_ACTION_PATH + '/create_github_token.js')
47+
script({core, audience})
48+
console.log("GitHub token creation done!")
4849
49-
- id: get-github-jwt-auth-token
50+
- id: get-github-jwt-token-vault
5051
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
5152
env:
5253
VAULT_INSTANCE: ${{ inputs.vault_instance }}
5354
with:
5455
script: |
55-
const jwt = await core.getIDToken("https://vault-github-actions.grafana-"+process.env.VAULT_INSTANCE+".net");
56-
core.setSecret(jwt);
57-
core.setOutput("github-jwt",jwt);
56+
const audience = "https://vault-github-actions.grafana-" + process.env.VAULT_INSTANCE + ".net"
57+
const script = require(process.env.GITHUB_ACTION_PATH + '/create_github_token.js')
58+
script({core, audience})
59+
console.log("GitHub token creation done!")
5860
5961
- name: Authenticate with Vault
6062
id: auth-vault
@@ -64,13 +66,11 @@ runs:
6466
REPOSITORY_NAME: ${{ github.event.repository.name }}
6567
PERMISSION_SET: ${{ inputs.permission_set}}
6668
VAULT_URL: "https://vault-github-actions.grafana-${{ inputs.vault_instance }}.net"
69+
GITHUB_JWT_PROXY: ${{ steps.get-github-jwt-token-proxy.outputs.github-jwt }}
70+
GITHUB_JWT_VAULT: ${{ steps.get-github-jwt-token-vault.outputs.github-jwt }}
71+
REF_SHA: ${{ steps.normalize-workflow-name.outputs.ref_sha }}
6772
run: |
68-
echo "${REPOSITORY_NAME}-${{ steps.normalize-workflow-name.outputs.ref_sha }}-${PERMISSION_SET}"
69-
curl --fail -X POST "${VAULT_URL}/v1/auth/github-actions-oidc/login" \
70-
-H "Content-Type: application/json" \
71-
-H "Proxy-Authorization-Token: Bearer ${{ steps.get-github-jwt-token.outputs.github-jwt }}" \
72-
-d "{\"role\": \"${REPOSITORY_NAME}-${{ steps.normalize-workflow-name.outputs.ref_sha }}-${PERMISSION_SET}\",\"jwt\": \"${{ steps.get-github-jwt-auth-token.outputs.github-jwt }}\"}" \
73-
| jq -r '"vault_token=\(.auth.client_token)"' >> $GITHUB_OUTPUT
73+
${GITHUB_ACTION_PATH}/auth_vault.sh
7474
7575
- name: Get GitHub Token
7676
id: generate-token
@@ -81,8 +81,8 @@ runs:
8181
PERMISSION_SET: ${{ inputs.permission_set}}
8282
GITHUB_APP: ${{ inputs.github_app }}
8383
VAULT_URL: "https://vault-github-actions.grafana-${{ inputs.vault_instance }}.net"
84+
VAULT_TOKEN: ${{ steps.auth-vault.outputs.vault_token }}
85+
GITHUB_JWT_PROXY: ${{ steps.get-github-jwt-token-proxy.outputs.github-jwt }}
86+
REF_SHA: ${{ steps.normalize-workflow-name.outputs.ref_sha }}
8487
run: |
85-
curl --fail "{$VAULT_URL}/v1/github-app-${GITHUB_APP}/token/${REPOSITORY_NAME}-${{ steps.normalize-workflow-name.outputs.ref_sha }}-${PERMISSION_SET}" \
86-
-H "X-Vault-Token: ${{ steps.auth-vault.outputs.vault_token }}" \
87-
-H "Proxy-Authorization-Token: Bearer ${{ steps.get-github-jwt-token.outputs.github-jwt }}" \
88-
| jq -r '"github_token=\(.data.token)"' >> $GITHUB_OUTPUT
88+
${GITHUB_ACTION_PATH}/create_token.sh
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
MAX_ATTEMPTS=3
5+
for attempt in $(seq 1 "${MAX_ATTEMPTS}"); do
6+
echo "Attempt ${attempt} to authenticate with Vault..."
7+
8+
RESPONSE=$(curl -sS -w "%{http_code}" -o response.json \
9+
-X POST "${VAULT_URL}/v1/auth/github-actions-oidc/login" \
10+
-H "Content-Type: application/json" \
11+
-H "Proxy-Authorization-Token: Bearer ${GITHUB_JWT_PROXY}" \
12+
-d "{
13+
\"role\": \"${REPOSITORY_NAME}-${REF_SHA}-${PERMISSION_SET}\",
14+
\"jwt\": \"${GITHUB_JWT_VAULT}\"
15+
}" || true)
16+
17+
if [[ "${RESPONSE}" -eq 200 ]]; then
18+
TOKEN=$(jq -r '.auth.client_token' response.json)
19+
echo "::add-mask::$TOKEN"
20+
echo "vault_token=${TOKEN}" >> "${GITHUB_OUTPUT}"
21+
echo "Vault auth done!"
22+
exit 0
23+
else
24+
echo "Vault auth failed (HTTP ${RESPONSE})"
25+
cat response.json || true
26+
sleep $((attempt * 5))
27+
fi
28+
done
29+
30+
echo "Failed to authenticate with Vault after ${MAX_ATTEMPTS} attempts"
31+
exit 1
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = ({ core, audience }) => {
2+
async function retry(fn, retries = 3, delay = 5000) {
3+
console.log(`Audience: ${audience}`);
4+
for (let i = 0; i < retries; i++) {
5+
console.log(`Attempt ${i + 1}`);
6+
try {
7+
return await fn();
8+
} catch (err) {
9+
core.warning(`Attempt ${i + 1} failed: ${err.message}`);
10+
if (i < retries - 1) {
11+
await new Promise((r) => setTimeout(r, delay * i));
12+
} else {
13+
throw err;
14+
}
15+
}
16+
}
17+
}
18+
19+
return retry(() => core.getIDToken(audience))
20+
.then((jwt) => {
21+
core.setSecret(jwt);
22+
core.setOutput("github-jwt", jwt);
23+
})
24+
.catch((err) => {
25+
core.setFailed(`Failed to get ID token: ${err.message}`);
26+
throw err; // ensure caller knows it failed
27+
});
28+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
MAX_ATTEMPTS=3
5+
IFS=',' read -ra APPS <<< "${GITHUB_APP}"
6+
setRandomApp() {
7+
GITHUB_APP=$(printf "%s\n" "${APPS[@]}" | sed 's/^ *//;s/ *$//' | shuf -n1)
8+
echo "Randomly selected GitHub App: ${GITHUB_APP}"
9+
}
10+
11+
for attempt in $(seq 1 "${MAX_ATTEMPTS}"); do
12+
echo "Attempt ${attempt} to get GitHub token..."
13+
setRandomApp
14+
RESPONSE=$(curl -sS -w "%{http_code}" -o response.json \
15+
"${VAULT_URL}/v1/github-app-${GITHUB_APP}/token/${REPOSITORY_NAME}-${REF_SHA}-${PERMISSION_SET}" \
16+
-H "X-Vault-Token: ${VAULT_TOKEN}" \
17+
-H "Proxy-Authorization-Token: Bearer ${GITHUB_JWT_PROXY}" || true)
18+
19+
if [[ "${RESPONSE}" -eq 200 ]]; then
20+
TOKEN=$(jq -r '.data.token' response.json)
21+
echo "github_token=${TOKEN}" >> "${GITHUB_OUTPUT}"
22+
echo "Create GitHub Token done!"
23+
exit 0
24+
else
25+
echo "Vault request failed (HTTP ${RESPONSE})"
26+
cat response.json || true
27+
sleep $((attempt * 5))
28+
fi
29+
done
30+
31+
echo "Failed to retrieve GitHub token after ${MAX_ATTEMPTS} attempts"
32+
exit 1

0 commit comments

Comments
 (0)