Skip to content

Commit 2046c0b

Browse files
PREQ-3880 JS wrapper for cache
1 parent 2b973d8 commit 2046c0b

File tree

17 files changed

+1255
-160
lines changed

17 files changed

+1255
-160
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
.claude
2+
node_modules/
3+
*.log
4+
.DS_Store

action.yml

Lines changed: 9 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -15,181 +15,30 @@ inputs:
1515
description: The chunk size used to split up large files during upload, in bytes
1616
enableCrossOsArchive:
1717
description: When enabled, allows to save or restore caches that can be restored or saved respectively on other platforms
18-
default: false
18+
default: 'false'
1919
fail-on-cache-miss:
2020
description: Fail the workflow if cache entry is not found
21-
default: false
21+
default: 'false'
2222
lookup-only:
2323
description: Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache
24-
default: false
24+
default: 'false'
2525
environment:
26-
description: Environment to use ('dev' or 'prod', 's3' backend only).
26+
description: Environment to use ('dev' or 'prod', 's3' backend only)
2727
default: prod
2828
fallback-branch:
29-
description: Optional maintenance branch for fallback restore keys (pattern 'branch-*', 's3' backend only). If not set, the repository
30-
default branch is used.
29+
description: Optional maintenance branch for fallback restore keys (pattern 'branch-*', 's3' backend only). If not set, the repository default branch is used.
3130
backend:
3231
description: Force cache backend ('github' or 's3'). If not set, automatically determined based on repository visibility.
3332

3433
outputs:
3534
cache-hit:
3635
description: A boolean value to indicate an exact match was found for the primary key
37-
value: ${{ steps.github-cache.outputs.cache-hit || steps.s3-cache.outputs.cache-hit }}
3836

3937
runs:
40-
using: composite
41-
steps:
42-
- name: Determine cache backend
43-
id: cache-backend
44-
shell: bash
45-
env:
46-
GITHUB_TOKEN: ${{ github.token }}
47-
REPO_VISIBILITY: ${{ github.event.repository.visibility }}
48-
FORCED_BACKEND: ${{ inputs.backend }}
49-
run: |
50-
if [[ "$FORCED_BACKEND" == "github" || "$FORCED_BACKEND" == "s3" ]]; then
51-
CACHE_BACKEND="$FORCED_BACKEND"
52-
echo "Using forced backend: $CACHE_BACKEND"
53-
else
54-
# If visibility is not available in the event, try to get it from the API
55-
if [[ -z "$REPO_VISIBILITY" || "$REPO_VISIBILITY" = "null" ]]; then
56-
REPO_VISIBILITY=$(curl -s -H "Authorization: token ${{ github.token }}" \
57-
"https://api.github.com/repos/${{ github.repository }}" | \
58-
jq -r '.visibility // "private"')
59-
fi
60-
echo "Repository visibility: $REPO_VISIBILITY"
61-
62-
if [[ "$REPO_VISIBILITY" == "public" ]]; then
63-
CACHE_BACKEND="github"
64-
echo "Using GitHub cache for public repository"
65-
else
66-
CACHE_BACKEND="s3"
67-
echo "Using S3 cache for private/internal repository"
68-
fi
69-
fi
70-
71-
echo "cache-backend=$CACHE_BACKEND" >> "$GITHUB_OUTPUT"
72-
73-
- name: Cache with GitHub Actions (public repos)
74-
if: steps.cache-backend.outputs.cache-backend == 'github'
75-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
76-
id: github-cache
77-
with:
78-
path: ${{ inputs.path }}
79-
key: ${{ inputs.key }}
80-
restore-keys: ${{ inputs.restore-keys }}
81-
upload-chunk-size: ${{ inputs.upload-chunk-size }}
82-
enableCrossOsArchive: ${{ inputs.enableCrossOsArchive }}
83-
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
84-
lookup-only: ${{ inputs.lookup-only }}
85-
86-
# Cache with S3 (private/internal repos)
87-
- name: Authenticate to AWS
88-
if: steps.cache-backend.outputs.cache-backend == 's3'
89-
id: aws-auth
90-
shell: bash
91-
env:
92-
POOL_ID: ${{ inputs.environment == 'prod' && 'eu-central-1:511fe374-ae4f-46d0-adb7-9246e570c7f4' || 'eu-central-1:3221c6ea-3f67-4fd8-a7ff-7426f96add89' }}
93-
AWS_ACCOUNT_ID: ${{ inputs.environment == 'prod' && '275878209202' || '460386131003' }}
94-
IDENTITY_PROVIDER_NAME: token.actions.githubusercontent.com
95-
AUDIENCE: cognito-identity.amazonaws.com
96-
AWS_REGION: eu-central-1
97-
GITHUB_RUN_ID: ${{ github.run_id }}
98-
run: |
99-
# Get GitHub Actions ID token using script
100-
ACCESS_TOKEN=$("$GITHUB_ACTION_PATH/scripts/get-github-token.sh")
101-
echo "::add-mask::$ACCESS_TOKEN"
102-
103-
# Get Identity ID
104-
identityId=$(aws cognito-identity get-id \
105-
--identity-pool-id "$POOL_ID" \
106-
--account-id "$AWS_ACCOUNT_ID" \
107-
--logins '{"'"$IDENTITY_PROVIDER_NAME"'":"'"$ACCESS_TOKEN"'"}' \
108-
--query 'IdentityId' --output text)
109-
110-
# Validate Identity ID was obtained
111-
if [[ "$identityId" == "null" || -z "$identityId" ]]; then
112-
echo "::error::Failed to obtain Identity ID from Cognito Identity Pool"
113-
echo "::error::Check identity pool configuration and IAM roles"
114-
exit 1
115-
fi
116-
117-
# Get and validate AWS credentials
118-
awsCredentials=$(aws cognito-identity get-credentials-for-identity \
119-
--identity-id "$identityId" \
120-
--logins '{"'"$IDENTITY_PROVIDER_NAME"'":"'"$ACCESS_TOKEN"'"}')
121-
122-
AWS_ACCESS_KEY_ID=$(echo "$awsCredentials" | jq -r ".Credentials.AccessKeyId")
123-
AWS_SECRET_ACCESS_KEY=$(echo "$awsCredentials" | jq -r ".Credentials.SecretKey")
124-
AWS_SESSION_TOKEN=$(echo "$awsCredentials" | jq -r ".Credentials.SessionToken")
125-
if [[ "$AWS_ACCESS_KEY_ID" == "null" || -z "$AWS_ACCESS_KEY_ID" ]]; then
126-
echo "::error::Failed to obtain AWS Access Key ID"
127-
exit 1
128-
fi
129-
if [[ "$AWS_SECRET_ACCESS_KEY" == "null" || -z "$AWS_SECRET_ACCESS_KEY" ]]; then
130-
echo "::error::Failed to obtain AWS Secret Access Key"
131-
exit 1
132-
fi
133-
if [[ "$AWS_SESSION_TOKEN" == "null" || -z "$AWS_SESSION_TOKEN" ]]; then
134-
echo "::error::Failed to obtain AWS Session Token"
135-
exit 1
136-
fi
137-
echo "::add-mask::$AWS_ACCESS_KEY_ID"
138-
echo "::add-mask::$AWS_SECRET_ACCESS_KEY"
139-
echo "::add-mask::$AWS_SESSION_TOKEN"
140-
141-
# Create a unique AWS profile to isolate credentials from user-configured AWS credentials
142-
# This prevents credential override when users call aws-actions/configure-aws-credentials
143-
# between the cache restore (main step) and cache save (post step)
144-
PROFILE_NAME="gh-action-cache-${GITHUB_RUN_ID}"
145-
146-
mkdir -p ~/.aws
147-
chmod 700 ~/.aws
148-
149-
# Write credentials to a dedicated profile using AWS CLI (handles file format and permissions correctly)
150-
aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" --profile "$PROFILE_NAME"
151-
aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" --profile "$PROFILE_NAME"
152-
aws configure set aws_session_token "$AWS_SESSION_TOKEN" --profile "$PROFILE_NAME"
153-
aws configure set region eu-central-1 --profile "$PROFILE_NAME"
154-
echo "Created AWS profile: $PROFILE_NAME"
155-
echo "AWS_PROFILE=$PROFILE_NAME" >> "$GITHUB_OUTPUT"
156-
# Export to GITHUB_ENV so the profile persists to post steps (cache save)
157-
# This is necessary because step-level env vars don't persist to post steps
158-
echo "AWS_PROFILE=$PROFILE_NAME" >> "$GITHUB_ENV"
159-
echo "AWS_DEFAULT_PROFILE=$PROFILE_NAME" >> "$GITHUB_ENV"
160-
161-
- name: Prepare cache keys
162-
if: steps.cache-backend.outputs.cache-backend == 's3'
163-
shell: bash
164-
id: prepare-keys
165-
env:
166-
INPUT_KEY: ${{ inputs.key }}
167-
INPUT_RESTORE_KEYS: ${{ inputs.restore-keys }}
168-
INPUT_FALLBACK_BRANCH: ${{ inputs.fallback-branch }}
169-
GITHUB_TOKEN: ${{ github.token }}
170-
GITHUB_REPOSITORY: ${{ github.repository }}
171-
run: $GITHUB_ACTION_PATH/scripts/prepare-keys.sh
172-
173-
- name: Cache on S3
174-
if: steps.cache-backend.outputs.cache-backend == 's3'
175-
uses: runs-on/cache@50350ad4242587b6c8c2baa2e740b1bc11285ff4 # v4.3.0
176-
id: s3-cache
177-
env:
178-
RUNS_ON_S3_BUCKET_CACHE: sonarsource-s3-cache-${{ inputs.environment }}-bucket
179-
AWS_DEFAULT_REGION: eu-central-1
180-
AWS_REGION: eu-central-1
181-
# Use AWS profile instead of direct credentials to prevent override issues
182-
# When users configure their own AWS credentials mid-job, the profile remains isolated
183-
AWS_PROFILE: ${{ steps.aws-auth.outputs.AWS_PROFILE }}
184-
AWS_DEFAULT_PROFILE: ${{ steps.aws-auth.outputs.AWS_PROFILE }}
185-
with:
186-
path: ${{ inputs.path }}
187-
key: ${{ steps.prepare-keys.outputs.branch-key }}
188-
restore-keys: ${{ steps.prepare-keys.outputs.branch-restore-keys }}
189-
upload-chunk-size: ${{ inputs.upload-chunk-size }}
190-
enableCrossOsArchive: ${{ inputs.enableCrossOsArchive }}
191-
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
192-
lookup-only: ${{ inputs.lookup-only }}
38+
using: 'node20'
39+
main: 'dist/main/index.js'
40+
post: 'dist/post/index.js'
41+
post-if: success()
19342

19443
branding:
19544
icon: upload-cloud

package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "gh-action-cache",
3+
"version": "2.0.0",
4+
"private": true,
5+
"description": "Cache files on S3 with branch-specific paths for granular permissions",
6+
"scripts": {
7+
"build": "npm run build:main && npm run build:post",
8+
"build:main": "ncc build src/main.ts -o dist/main --source-map",
9+
"build:post": "ncc build src/post.ts -o dist/post --source-map",
10+
"test": "jest",
11+
"lint": "eslint src/**/*.ts",
12+
"all": "npm run lint && npm run test && npm run build"
13+
},
14+
"dependencies": {
15+
"@actions/cache": "^3.2.4",
16+
"@actions/core": "^1.10.1",
17+
"@actions/github": "^6.0.0",
18+
"@actions/http-client": "^2.2.0",
19+
"@aws-sdk/client-cognito-identity": "^3.500.0",
20+
"@aws-sdk/client-s3": "^3.500.0",
21+
"tar": "^6.2.0"
22+
},
23+
"devDependencies": {
24+
"@types/jest": "^29.5.11",
25+
"@types/node": "^20.10.6",
26+
"@types/tar": "^6.1.11",
27+
"@vercel/ncc": "^0.38.1",
28+
"eslint": "^8.56.0",
29+
"@typescript-eslint/eslint-plugin": "^6.18.0",
30+
"@typescript-eslint/parser": "^6.18.0",
31+
"jest": "^29.7.0",
32+
"ts-jest": "^29.1.1",
33+
"typescript": "^5.3.3"
34+
}
35+
}

src/auth/cognito.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* AWS Cognito Authentication
3+
* Exchanges GitHub OIDC token for AWS credentials
4+
*/
5+
6+
import * as core from '@actions/core';
7+
import {
8+
CognitoIdentityClient,
9+
GetIdCommand,
10+
GetCredentialsForIdentityCommand,
11+
} from '@aws-sdk/client-cognito-identity';
12+
import { getGitHubOidcToken } from './oidc';
13+
import { AwsCredentials, CognitoConfig } from '../types';
14+
15+
const COGNITO_CONFIG: Record<'prod' | 'dev', CognitoConfig> = {
16+
prod: {
17+
poolId: 'eu-central-1:511fe374-ae4f-46d0-adb7-9246e570c7f4',
18+
accountId: '275878209202',
19+
region: 'eu-central-1',
20+
},
21+
dev: {
22+
poolId: 'eu-central-1:3221c6ea-3f67-4fd8-a7ff-7426f96add89',
23+
accountId: '460386131003',
24+
region: 'eu-central-1',
25+
},
26+
};
27+
28+
const IDENTITY_PROVIDER = 'token.actions.githubusercontent.com';
29+
const COGNITO_AUDIENCE = 'cognito-identity.amazonaws.com';
30+
31+
/**
32+
* Authenticate to AWS using GitHub OIDC and Cognito Identity Pool
33+
*/
34+
export async function authenticateAws(
35+
environment: 'prod' | 'dev'
36+
): Promise<AwsCredentials> {
37+
const config = COGNITO_CONFIG[environment];
38+
39+
core.info(`Authenticating to AWS Cognito (${environment} environment)...`);
40+
41+
// Create Cognito client without credentials (we're using OIDC)
42+
const cognitoClient = new CognitoIdentityClient({
43+
region: config.region,
44+
});
45+
46+
// Step 1: Get GitHub OIDC token
47+
core.debug('Requesting GitHub OIDC token...');
48+
const oidcToken = await getGitHubOidcToken(COGNITO_AUDIENCE);
49+
core.debug('OIDC token obtained successfully');
50+
51+
// Step 2: Get Cognito Identity ID
52+
core.debug('Getting Cognito Identity ID...');
53+
const getIdResponse = await cognitoClient.send(
54+
new GetIdCommand({
55+
IdentityPoolId: config.poolId,
56+
AccountId: config.accountId,
57+
Logins: {
58+
[IDENTITY_PROVIDER]: oidcToken,
59+
},
60+
})
61+
);
62+
63+
if (!getIdResponse.IdentityId) {
64+
throw new Error(
65+
'Failed to obtain Cognito Identity ID. ' +
66+
'Check identity pool configuration and IAM trust policy.'
67+
);
68+
}
69+
70+
core.debug(`Identity ID: ${getIdResponse.IdentityId}`);
71+
72+
// Step 3: Get AWS credentials
73+
core.debug('Getting AWS credentials from Cognito...');
74+
const getCredentialsResponse = await cognitoClient.send(
75+
new GetCredentialsForIdentityCommand({
76+
IdentityId: getIdResponse.IdentityId,
77+
Logins: {
78+
[IDENTITY_PROVIDER]: oidcToken,
79+
},
80+
})
81+
);
82+
83+
const credentials = getCredentialsResponse.Credentials;
84+
if (
85+
!credentials?.AccessKeyId ||
86+
!credentials?.SecretKey ||
87+
!credentials?.SessionToken
88+
) {
89+
throw new Error(
90+
'Failed to obtain AWS credentials from Cognito. ' +
91+
'Check IAM role configuration and permissions.'
92+
);
93+
}
94+
95+
// Mask credentials in logs
96+
core.setSecret(credentials.AccessKeyId);
97+
core.setSecret(credentials.SecretKey);
98+
core.setSecret(credentials.SessionToken);
99+
100+
core.info('AWS authentication successful');
101+
102+
return {
103+
accessKeyId: credentials.AccessKeyId,
104+
secretAccessKey: credentials.SecretKey,
105+
sessionToken: credentials.SessionToken,
106+
region: config.region,
107+
};
108+
}

src/auth/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Authentication module exports
3+
*/
4+
5+
export { getGitHubOidcToken } from './oidc';
6+
export { authenticateAws } from './cognito';

0 commit comments

Comments
 (0)