Skip to content

Commit 73e2f69

Browse files
authored
Merge pull request #132 from LerianStudio/s3-upload
feat(ci): add reusable S3 upload workflow with environment detection and AWS integration
2 parents 5cdd9c8 + fddd8c4 commit 73e2f69

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed

.github/dependabot.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ updates:
9393
- "minor"
9494
- "patch"
9595

96+
# AWS actions (OIDC, credentials, S3)
97+
aws:
98+
patterns:
99+
- "aws-actions/*"
100+
update-types:
101+
- "major"
102+
- "minor"
103+
- "patch"
104+
96105
# Miscellaneous third-party utilities
97106
utilities:
98107
patterns:

.github/workflows/s3-upload.yml

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
name: "S3 Upload"
2+
3+
# Reusable workflow for uploading files to S3 with environment-based folder routing.
4+
#
5+
# Features:
6+
# - Automatic environment detection from tag suffix (beta → development, rc → staging, release → production)
7+
# - Supports glob patterns for flexible file selection
8+
# - AWS authentication via OIDC (IAM Role)
9+
# - Optional custom S3 prefix within environment folder
10+
# - dry_run mode for previewing uploads without applying
11+
#
12+
# Examples:
13+
# # Upload Casdoor init data
14+
# uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@v1.0.0
15+
# with:
16+
# s3_bucket: "lerian-casdoor-init-data"
17+
# file_pattern: "init/casdoor/init_data*.json"
18+
# secrets:
19+
# AWS_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }}
20+
#
21+
# # Upload migration files with custom prefix
22+
# uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@v1.0.0
23+
# with:
24+
# s3_bucket: "lerian-migration-files"
25+
# file_pattern: "init/casdoor-migrations/migrations/*.sql"
26+
# s3_prefix: "casdoor-migrations"
27+
# secrets:
28+
# AWS_ROLE_ARN: ${{ secrets.AWS_MIGRATIONS_ROLE_ARN }}
29+
30+
on:
31+
workflow_call:
32+
inputs:
33+
runner_type:
34+
description: 'Runner to use for the workflow'
35+
type: string
36+
default: 'blacksmith-4vcpu-ubuntu-2404'
37+
s3_bucket:
38+
description: 'S3 bucket name (without s3:// prefix)'
39+
type: string
40+
required: true
41+
file_pattern:
42+
description: 'Glob pattern for files to upload (e.g., "init/casdoor/init_data*.json", "migrations/*.sql")'
43+
type: string
44+
required: true
45+
s3_prefix:
46+
description: 'Optional prefix inside the environment folder (e.g., "casdoor-migrations" → development/casdoor-migrations/)'
47+
type: string
48+
default: ''
49+
aws_region:
50+
description: 'AWS region'
51+
type: string
52+
default: 'us-east-2'
53+
environment_detection:
54+
description: 'Environment detection strategy: tag_suffix (auto from tag) or manual'
55+
type: string
56+
default: 'tag_suffix'
57+
manual_environment:
58+
description: 'Manually specify environment (development/staging/production) - only used if environment_detection is manual'
59+
type: string
60+
required: false
61+
flatten:
62+
description: 'Upload files without preserving directory structure (only filenames)'
63+
type: boolean
64+
default: true
65+
dry_run:
66+
description: 'Preview changes without applying them'
67+
type: boolean
68+
required: false
69+
default: false
70+
secrets:
71+
AWS_ROLE_ARN:
72+
description: 'ARN of the IAM role to assume for S3 access'
73+
required: true
74+
workflow_dispatch:
75+
inputs:
76+
runner_type:
77+
description: 'Runner to use for the workflow'
78+
type: string
79+
default: 'blacksmith-4vcpu-ubuntu-2404'
80+
s3_bucket:
81+
description: 'S3 bucket name (without s3:// prefix)'
82+
type: string
83+
required: true
84+
file_pattern:
85+
description: 'Glob pattern for files to upload (e.g., "init/casdoor/init_data*.json", "migrations/*.sql")'
86+
type: string
87+
required: true
88+
s3_prefix:
89+
description: 'Optional prefix inside the environment folder'
90+
type: string
91+
default: ''
92+
aws_region:
93+
description: 'AWS region'
94+
type: string
95+
default: 'us-east-2'
96+
environment_detection:
97+
description: 'Environment detection strategy: tag_suffix (auto from tag) or manual'
98+
type: string
99+
default: 'tag_suffix'
100+
manual_environment:
101+
description: 'Manually specify environment (development/staging/production)'
102+
type: string
103+
required: false
104+
flatten:
105+
description: 'Upload files without preserving directory structure (only filenames)'
106+
type: boolean
107+
default: true
108+
dry_run:
109+
description: 'Dry run — preview uploads without applying them'
110+
type: boolean
111+
default: false
112+
113+
permissions:
114+
id-token: write
115+
contents: read
116+
117+
jobs:
118+
upload:
119+
runs-on: ${{ inputs.runner_type }}
120+
steps:
121+
- uses: actions/checkout@v4
122+
123+
- name: Determine environment
124+
id: env
125+
env:
126+
INPUT_ENV_DETECTION: ${{ inputs.environment_detection }}
127+
INPUT_MANUAL_ENV: ${{ inputs.manual_environment }}
128+
run: |
129+
set -euo pipefail
130+
131+
if [[ "$INPUT_ENV_DETECTION" == "manual" ]]; then
132+
case "$INPUT_MANUAL_ENV" in
133+
development|staging|production)
134+
FOLDER="$INPUT_MANUAL_ENV"
135+
;;
136+
*)
137+
echo "::error::manual_environment must be one of: development, staging, production (got '${INPUT_MANUAL_ENV}')"
138+
exit 1
139+
;;
140+
esac
141+
elif [[ "$INPUT_ENV_DETECTION" == "tag_suffix" ]]; then
142+
REF="${GITHUB_REF#refs/}"
143+
TAG="${GITHUB_REF#refs/tags/}"
144+
145+
if [[ "$REF" == heads/develop ]] || [[ "$TAG" == *-beta* ]]; then
146+
FOLDER="development"
147+
elif [[ "$REF" == heads/release-candidate ]] || [[ "$TAG" == *-rc* ]]; then
148+
FOLDER="staging"
149+
elif [[ "$REF" == heads/main ]] || [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
150+
FOLDER="production"
151+
else
152+
echo "⚠️ Ref '$REF' does not match any environment. Skipping upload."
153+
FOLDER=""
154+
fi
155+
else
156+
echo "::error::environment_detection must be one of: tag_suffix, manual (got '${INPUT_ENV_DETECTION}')"
157+
exit 1
158+
fi
159+
160+
echo "folder=${FOLDER}" >> "$GITHUB_OUTPUT"
161+
[[ -n "$FOLDER" ]] && echo "📁 Environment: ${FOLDER}" || echo "⚠️ No environment matched"
162+
163+
- name: Configure AWS credentials
164+
if: steps.env.outputs.folder != ''
165+
uses: aws-actions/configure-aws-credentials@v4
166+
with:
167+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
168+
aws-region: ${{ inputs.aws_region }}
169+
170+
- name: Dry run summary
171+
if: steps.env.outputs.folder != '' && inputs.dry_run
172+
env:
173+
BUCKET: ${{ inputs.s3_bucket }}
174+
FOLDER: ${{ steps.env.outputs.folder }}
175+
PREFIX: ${{ inputs.s3_prefix }}
176+
PATTERN: ${{ inputs.file_pattern }}
177+
FLATTEN: ${{ inputs.flatten }}
178+
run: |
179+
set -euo pipefail
180+
181+
echo "::notice::DRY RUN — no files will be uploaded"
182+
echo " bucket : ${BUCKET}"
183+
echo " folder : ${FOLDER}"
184+
echo " prefix : ${PREFIX:-<none>}"
185+
echo " pattern : ${PATTERN}"
186+
echo " flatten : ${FLATTEN}"
187+
188+
if [[ -n "$PREFIX" ]]; then
189+
S3_PATH="s3://${BUCKET}/${FOLDER}/${PREFIX}/"
190+
else
191+
S3_PATH="s3://${BUCKET}/${FOLDER}/"
192+
fi
193+
194+
shopt -s nullglob
195+
FILE_COUNT=0
196+
197+
for file in $PATTERN; do
198+
if [[ "$FLATTEN" == "true" ]]; then
199+
echo " [dry-run] aws s3 cp $file ${S3_PATH}"
200+
else
201+
echo " [dry-run] aws s3 cp $file ${S3_PATH}${file}"
202+
fi
203+
FILE_COUNT=$((FILE_COUNT + 1))
204+
done
205+
206+
if [[ $FILE_COUNT -eq 0 ]]; then
207+
echo "::warning::No files matched pattern: ${PATTERN}"
208+
exit 1
209+
fi
210+
211+
echo "::notice::Would upload ${FILE_COUNT} file(s) to ${S3_PATH}"
212+
213+
- name: Upload files to S3
214+
if: steps.env.outputs.folder != '' && !inputs.dry_run
215+
env:
216+
BUCKET: ${{ inputs.s3_bucket }}
217+
FOLDER: ${{ steps.env.outputs.folder }}
218+
PREFIX: ${{ inputs.s3_prefix }}
219+
PATTERN: ${{ inputs.file_pattern }}
220+
FLATTEN: ${{ inputs.flatten }}
221+
run: |
222+
set -euo pipefail
223+
224+
# Build S3 destination path
225+
if [[ -n "$PREFIX" ]]; then
226+
S3_PATH="s3://${BUCKET}/${FOLDER}/${PREFIX}/"
227+
else
228+
S3_PATH="s3://${BUCKET}/${FOLDER}/"
229+
fi
230+
231+
# Find and upload files
232+
shopt -s nullglob
233+
FILE_COUNT=0
234+
235+
for file in $PATTERN; do
236+
if [[ "$FLATTEN" == "true" ]]; then
237+
aws s3 cp "$file" "${S3_PATH}"
238+
else
239+
aws s3 cp "$file" "${S3_PATH}${file}"
240+
fi
241+
242+
FILE_COUNT=$((FILE_COUNT + 1))
243+
done
244+
245+
if [[ $FILE_COUNT -eq 0 ]]; then
246+
echo "⚠️ No files matched pattern: ${PATTERN}"
247+
exit 1
248+
fi
249+
250+
echo "::notice::Uploaded ${FILE_COUNT} file(s) to ${S3_PATH}"

docs/s3-upload.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<table border="0" cellspacing="0" cellpadding="0">
2+
<tr>
3+
<td><img src="https://github.com/LerianStudio.png" width="72" alt="Lerian" /></td>
4+
<td><h1>s3-upload</h1></td>
5+
</tr>
6+
</table>
7+
8+
Reusable workflow for uploading files to AWS S3 with automatic environment-based folder routing and OIDC authentication.
9+
10+
## What it does
11+
12+
Uploads files matching a glob pattern to an S3 bucket, organized by environment folder. The environment is detected automatically from the git ref/tag or can be set manually.
13+
14+
| Ref / Tag | Environment folder |
15+
|---|---|
16+
| `develop` branch or `*-beta*` tag | `development/` |
17+
| `release-candidate` branch or `*-rc*` tag | `staging/` |
18+
| `main` branch or `vX.Y.Z` tag | `production/` |
19+
20+
## Inputs
21+
22+
| Input | Type | Required | Default | Description |
23+
|---|---|:---:|---|---|
24+
| `runner_type` | `string` | No | `blacksmith-4vcpu-ubuntu-2404` | Runner to use for the workflow |
25+
| `s3_bucket` | `string` | **Yes** || S3 bucket name (without `s3://` prefix) |
26+
| `file_pattern` | `string` | **Yes** || Glob pattern for files to upload |
27+
| `s3_prefix` | `string` | No | `""` | Optional prefix inside the environment folder |
28+
| `aws_region` | `string` | No | `us-east-2` | AWS region |
29+
| `environment_detection` | `string` | No | `tag_suffix` | Detection strategy: `tag_suffix` or `manual` |
30+
| `manual_environment` | `string` | No || Environment override: `development`, `staging`, or `production` |
31+
| `flatten` | `boolean` | No | `true` | Upload only filenames (discard directory structure) |
32+
| `dry_run` | `boolean` | No | `false` | Preview uploads without applying them |
33+
34+
## Secrets
35+
36+
| Secret | Required | Description |
37+
|---|---|---|
38+
| `AWS_ROLE_ARN` | **Yes** | ARN of the IAM role to assume via OIDC for S3 access |
39+
40+
## Usage
41+
42+
### Upload init data files
43+
44+
```yaml
45+
jobs:
46+
upload:
47+
uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@v1.0.0
48+
with:
49+
s3_bucket: "lerian-casdoor-init-data"
50+
file_pattern: "init/casdoor/init_data*.json"
51+
secrets:
52+
AWS_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }}
53+
```
54+
55+
### Upload migration files with custom prefix
56+
57+
```yaml
58+
jobs:
59+
upload:
60+
uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@v1.0.0
61+
with:
62+
s3_bucket: "lerian-migration-files"
63+
file_pattern: "init/casdoor-migrations/migrations/*.sql"
64+
s3_prefix: "casdoor-migrations"
65+
secrets:
66+
AWS_ROLE_ARN: ${{ secrets.AWS_MIGRATIONS_ROLE_ARN }}
67+
```
68+
69+
### Dry run (preview only)
70+
71+
```yaml
72+
# Use @develop or your feature branch to validate before releasing
73+
jobs:
74+
preview:
75+
uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@develop
76+
with:
77+
s3_bucket: "lerian-casdoor-init-data"
78+
file_pattern: "init/casdoor/init_data*.json"
79+
dry_run: true
80+
secrets:
81+
AWS_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }}
82+
```
83+
84+
### Manual environment override
85+
86+
```yaml
87+
jobs:
88+
upload:
89+
uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@v1.0.0
90+
with:
91+
s3_bucket: "lerian-casdoor-init-data"
92+
file_pattern: "init/casdoor/init_data*.json"
93+
environment_detection: "manual"
94+
manual_environment: "staging"
95+
secrets:
96+
AWS_ROLE_ARN: ${{ secrets.AWS_INIT_DATA_ROLE_ARN }}
97+
```
98+
99+
### Preserve directory structure
100+
101+
```yaml
102+
jobs:
103+
upload:
104+
uses: LerianStudio/github-actions-shared-workflows/.github/workflows/s3-upload.yml@v1.0.0
105+
with:
106+
s3_bucket: "lerian-migration-files"
107+
file_pattern: "init/casdoor-migrations/migrations/*.sql"
108+
flatten: false
109+
secrets:
110+
AWS_ROLE_ARN: ${{ secrets.AWS_MIGRATIONS_ROLE_ARN }}
111+
```
112+
113+
## Permissions
114+
115+
```yaml
116+
permissions:
117+
id-token: write
118+
contents: read
119+
```

0 commit comments

Comments
 (0)