Skip to content

Commit fe5d85c

Browse files
CENG-442: Implement retry logic for OIDC
1 parent 6753d23 commit fe5d85c

File tree

6 files changed

+118
-67
lines changed

6 files changed

+118
-67
lines changed

Diff for: README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This GitHub Action installs the Cloudsmith CLI and pre-authenticates it using OI
1111
- [`oidc-namespace`](action.yml): Cloudsmith organisation/namespace for OIDC (optional). 🌐
1212
- [`oidc-service-slug`](action.yml): Cloudsmith service account slug for OIDC (optional). 🐌
1313
- [`oidc-auth-only`](action.yml): Only perform OIDC authentication without installing the CLI (optional, default: false). 🔐
14+
- [`oidc-auth-retry`](action.yml): Number of retry attempts for OIDC authentication (0-10), 5 seconds delay between retries (optional, default: 3). 🔄
1415
- [`pip-install`](action.yml): Install the Cloudsmith CLI via pip (optional). 🐍
1516
- [`executable-path`](action.yml): Path to the Cloudsmith CLI executable (optional, default: `GITHUB_WORKSPACE/bin/`). 🛠️
1617

@@ -26,7 +27,7 @@ This GitHub Action installs the Cloudsmith CLI and pre-authenticates it using OI
2627
Cloudsmith OIDC [documentation](https://help.cloudsmith.io/docs/openid-connect) 📚
2728

2829
```yaml
29-
uses: cloudsmith-io/[email protected].2
30+
uses: cloudsmith-io/[email protected].3
3031
with:
3132
oidc-namespace: 'your-oidc-namespace'
3233
oidc-service-slug: 'your-service-account-slug'
@@ -37,7 +38,7 @@ with:
3738
Personal API Key can be found [here](https://cloudsmith.io/user/settings/api/), for CI-CD deployments we recommend using [Service Accounts](https://help.cloudsmith.io/docs/service-accounts). 🔒
3839
3940
```yaml
40-
uses: cloudsmith-io/[email protected].2
41+
uses: cloudsmith-io/[email protected].3
4142
with:
4243
api-key: 'your-api-key'
4344
```
@@ -47,7 +48,7 @@ with:
4748
If you only need to authenticate with Cloudsmith's API without installing the CLI:
4849
4950
```yaml
50-
uses: cloudsmith-io/[email protected].2
51+
uses: cloudsmith-io/[email protected].3
5152
with:
5253
oidc-namespace: 'your-oidc-namespace'
5354
oidc-service-slug: 'your-service-account-slug'
@@ -86,7 +87,7 @@ jobs:
8687
uses: actions/checkout@v4
8788
8889
- name: Install Cloudsmith CLI
89-
uses: cloudsmith-io/[email protected].2
90+
uses: cloudsmith-io/[email protected].3
9091
with:
9192
oidc-namespace: 'your-oidc-namespace'
9293
oidc-service-slug: 'your-service-account-slug'

Diff for: action.yml

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ inputs:
2222
description: 'Only perform OIDC authentication without installing the CLI'
2323
default: 'false'
2424
required: false
25+
oidc-auth-retry:
26+
description: 'Number of retry attempts for OIDC authentication (0-10)'
27+
default: '3'
28+
required: false
2529
pip-install:
2630
description: 'Install the Cloudsmith CLI via pip'
2731
default: 'false'

Diff for: dist/index.js

+54-31
Original file line numberDiff line numberDiff line change
@@ -33431,52 +33431,74 @@ const axios = __nccwpck_require__(8757);
3343133431
const core = __nccwpck_require__(2186);
3343233432

3343333433
const DEFAULT_API_HOST = 'api.cloudsmith.io';
33434+
const RETRY_DELAY_MS = 5000; // 5 seconds delay between attempts
33435+
33436+
async function retryWithDelay(fn, retries) {
33437+
let lastError;
33438+
for (let attempt = 1; attempt <= retries; attempt++) {
33439+
try {
33440+
return await fn();
33441+
} catch (error) {
33442+
lastError = error;
33443+
if (attempt < retries) {
33444+
core.info(`OIDC authentication attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS/1000} seconds...`);
33445+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
33446+
}
33447+
}
33448+
}
33449+
throw lastError;
33450+
}
3343433451

3343533452
/**
3343633453
* Authenticates with Cloudsmith using OIDC and validates the token.
3343733454
*
3343833455
* @param {string} orgName - The organization name.
3343933456
* @param {string} serviceAccountSlug - The service account slug.
3344033457
* @param {string} apiHost - The API host to connect to (optional).
33458+
* @param {number} retryAttempts - Number of retry attempts for authentication (0-10).
3344133459
*/
33442-
async function authenticate(orgName, serviceAccountSlug, apiHost) {
33460+
async function authenticate(orgName, serviceAccountSlug, apiHost, retryAttempts = 3) {
3344333461
try {
33444-
// Retrieve the OIDC ID token from GitHub Actions
33445-
const idToken = await core.getIDToken("api://AzureADTokenExchange");
33462+
core.info(`Attempting OIDC authentication with ${retryAttempts} retry attempts...`);
3344633463

33447-
// Use the provided apiHost or default to api.cloudsmith.io
33448-
const baseUrl = `https://${apiHost || DEFAULT_API_HOST}`;
33464+
await retryWithDelay(async () => {
33465+
// Retrieve the OIDC ID token from GitHub Actions
33466+
const idToken = await core.getIDToken("api://AzureADTokenExchange");
3344933467

33450-
// Send a POST request to Cloudsmith API to authenticate using the OIDC token
33451-
const response = await axios.post(
33452-
`${baseUrl}/openid/${orgName}/`,
33453-
{
33454-
oidc_token: idToken,
33455-
service_slug: serviceAccountSlug,
33456-
},
33457-
{
33458-
headers: {
33459-
"Content-Type": "application/json",
33468+
// Use the provided apiHost or default to api.cloudsmith.io
33469+
const baseUrl = `https://${apiHost || DEFAULT_API_HOST}`;
33470+
33471+
// Send a POST request to Cloudsmith API to authenticate using the OIDC token
33472+
const response = await axios.post(
33473+
`${baseUrl}/openid/${orgName}/`,
33474+
{
33475+
oidc_token: idToken,
33476+
service_slug: serviceAccountSlug,
3346033477
},
33461-
}
33462-
);
33478+
{
33479+
headers: {
33480+
"Content-Type": "application/json",
33481+
},
33482+
}
33483+
);
3346333484

33464-
// Check if the authentication was successful
33465-
if (response.status !== 200 && response.status !== 201) {
33466-
throw new Error(`Failed to authenticate: ${response.statusText}`);
33467-
}
33485+
// Check if the authentication was successful
33486+
if (response.status !== 200 && response.status !== 201) {
33487+
throw new Error(`Failed to authenticate: ${response.statusText}`);
33488+
}
3346833489

33469-
// Extract the API token from the response
33470-
const token = response.data.token;
33490+
// Extract the API token from the response
33491+
const token = response.data.token;
3347133492

33472-
// Export the API token as an environment variable
33473-
core.exportVariable("CLOUDSMITH_API_KEY", token);
33474-
core.info(
33475-
"Authenticated successfully with OIDC and saved JWT (token) to `CLOUDSMITH_API_KEY` environment variable."
33476-
);
33493+
// Export the API token as an environment variable
33494+
core.exportVariable("CLOUDSMITH_API_KEY", token);
33495+
core.info(
33496+
"Authenticated successfully with OIDC and saved JWT (token) to `CLOUDSMITH_API_KEY` environment variable."
33497+
);
3347733498

33478-
// Validate the token to ensure it is correct
33479-
await validateToken(token, baseUrl);
33499+
// Validate the token to ensure it is correct
33500+
await validateToken(token, baseUrl);
33501+
}, retryAttempts);
3348033502
} catch (error) {
3348133503
// Set the GitHub Action as failed if any error occurs
3348233504
core.setFailed(`OIDC authentication failed: ${error.message}`);
@@ -43051,6 +43073,7 @@ async function run() {
4305143073
const orgName = core.getInput('oidc-namespace');
4305243074
const serviceAccountSlug = core.getInput('oidc-service-slug');
4305343075
const apiKey = core.getInput('api-key');
43076+
const oidcAuthRetry = Math.min(Math.max(parseInt(core.getInput('oidc-auth-retry') || '3', 10), 0), 10);
4305443077

4305543078
// Cloudsmith CLI optional inputs
4305643079
const apiHost = core.getInput('api-host');
@@ -43068,7 +43091,7 @@ async function run() {
4306843091
core.exportVariable("CLOUDSMITH_API_KEY", apiKey);
4306943092
core.info("Using provided API key for authentication.");
4307043093
} else if (orgName && serviceAccountSlug) {
43071-
await oidcAuth.authenticate(orgName, serviceAccountSlug, apiHost);
43094+
await oidcAuth.authenticate(orgName, serviceAccountSlug, apiHost, oidcAuthRetry);
4307243095
} else {
4307343096
throw new Error("Either API key or OIDC inputs (namespace and service account slug) must be provided for authentication.");
4307443097
}

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cloudsmith-github-action",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "A GitHub Action to install Cloudsmith CLI and authenticate using OIDC",
55
"main": "dist/index.js",
66
"scripts": {

Diff for: src/main.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ async function run() {
99
const orgName = core.getInput('oidc-namespace');
1010
const serviceAccountSlug = core.getInput('oidc-service-slug');
1111
const apiKey = core.getInput('api-key');
12+
const oidcAuthRetry = Math.min(Math.max(parseInt(core.getInput('oidc-auth-retry') || '3', 10), 0), 10);
1213

1314
// Cloudsmith CLI optional inputs
1415
const apiHost = core.getInput('api-host');
@@ -26,7 +27,7 @@ async function run() {
2627
core.exportVariable("CLOUDSMITH_API_KEY", apiKey);
2728
core.info("Using provided API key for authentication.");
2829
} else if (orgName && serviceAccountSlug) {
29-
await oidcAuth.authenticate(orgName, serviceAccountSlug, apiHost);
30+
await oidcAuth.authenticate(orgName, serviceAccountSlug, apiHost, oidcAuthRetry);
3031
} else {
3132
throw new Error("Either API key or OIDC inputs (namespace and service account slug) must be provided for authentication.");
3233
}

Diff for: src/oidc-auth.js

+52-30
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,74 @@ const axios = require("axios");
22
const core = require("@actions/core");
33

44
const DEFAULT_API_HOST = 'api.cloudsmith.io';
5+
const RETRY_DELAY_MS = 5000; // 5 seconds delay between attempts
6+
7+
async function retryWithDelay(fn, retries) {
8+
let lastError;
9+
for (let attempt = 1; attempt <= retries; attempt++) {
10+
try {
11+
return await fn();
12+
} catch (error) {
13+
lastError = error;
14+
if (attempt < retries) {
15+
core.info(`OIDC authentication attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS/1000} seconds...`);
16+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
17+
}
18+
}
19+
}
20+
throw lastError;
21+
}
522

623
/**
724
* Authenticates with Cloudsmith using OIDC and validates the token.
825
*
926
* @param {string} orgName - The organization name.
1027
* @param {string} serviceAccountSlug - The service account slug.
1128
* @param {string} apiHost - The API host to connect to (optional).
29+
* @param {number} retryAttempts - Number of retry attempts for authentication (0-10).
1230
*/
13-
async function authenticate(orgName, serviceAccountSlug, apiHost) {
31+
async function authenticate(orgName, serviceAccountSlug, apiHost, retryAttempts = 3) {
1432
try {
15-
// Retrieve the OIDC ID token from GitHub Actions
16-
const idToken = await core.getIDToken("api://AzureADTokenExchange");
33+
core.info(`Attempting OIDC authentication with ${retryAttempts} retry attempts...`);
34+
35+
await retryWithDelay(async () => {
36+
// Retrieve the OIDC ID token from GitHub Actions
37+
const idToken = await core.getIDToken("api://AzureADTokenExchange");
1738

18-
// Use the provided apiHost or default to api.cloudsmith.io
19-
const baseUrl = `https://${apiHost || DEFAULT_API_HOST}`;
39+
// Use the provided apiHost or default to api.cloudsmith.io
40+
const baseUrl = `https://${apiHost || DEFAULT_API_HOST}`;
2041

21-
// Send a POST request to Cloudsmith API to authenticate using the OIDC token
22-
const response = await axios.post(
23-
`${baseUrl}/openid/${orgName}/`,
24-
{
25-
oidc_token: idToken,
26-
service_slug: serviceAccountSlug,
27-
},
28-
{
29-
headers: {
30-
"Content-Type": "application/json",
42+
// Send a POST request to Cloudsmith API to authenticate using the OIDC token
43+
const response = await axios.post(
44+
`${baseUrl}/openid/${orgName}/`,
45+
{
46+
oidc_token: idToken,
47+
service_slug: serviceAccountSlug,
3148
},
32-
}
33-
);
49+
{
50+
headers: {
51+
"Content-Type": "application/json",
52+
},
53+
}
54+
);
3455

35-
// Check if the authentication was successful
36-
if (response.status !== 200 && response.status !== 201) {
37-
throw new Error(`Failed to authenticate: ${response.statusText}`);
38-
}
56+
// Check if the authentication was successful
57+
if (response.status !== 200 && response.status !== 201) {
58+
throw new Error(`Failed to authenticate: ${response.statusText}`);
59+
}
3960

40-
// Extract the API token from the response
41-
const token = response.data.token;
61+
// Extract the API token from the response
62+
const token = response.data.token;
4263

43-
// Export the API token as an environment variable
44-
core.exportVariable("CLOUDSMITH_API_KEY", token);
45-
core.info(
46-
"Authenticated successfully with OIDC and saved JWT (token) to `CLOUDSMITH_API_KEY` environment variable."
47-
);
64+
// Export the API token as an environment variable
65+
core.exportVariable("CLOUDSMITH_API_KEY", token);
66+
core.info(
67+
"Authenticated successfully with OIDC and saved JWT (token) to `CLOUDSMITH_API_KEY` environment variable."
68+
);
4869

49-
// Validate the token to ensure it is correct
50-
await validateToken(token, baseUrl);
70+
// Validate the token to ensure it is correct
71+
await validateToken(token, baseUrl);
72+
}, retryAttempts);
5173
} catch (error) {
5274
// Set the GitHub Action as failed if any error occurs
5375
core.setFailed(`OIDC authentication failed: ${error.message}`);

0 commit comments

Comments
 (0)