Skip to content

Commit f78b334

Browse files
authored
feat: add workflow to publish releases file to aws s3 (#236)
1 parent 9e5345f commit f78b334

File tree

1 file changed

+170
-0
lines changed

1 file changed

+170
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: Mirror Releases to S3
2+
3+
on:
4+
release:
5+
types: [published] # Auto trigger when a release is published
6+
workflow_dispatch: # Manual trigger
7+
inputs:
8+
tag_name:
9+
description: "Release tag (e.g. v0.6.9). Takes precedence if both provided."
10+
required: false
11+
type: string
12+
release_name:
13+
description: "Release name (e.g. Jan 0.6.9). Used when tag_name is not provided."
14+
required: false
15+
type: string
16+
17+
jobs:
18+
mirror:
19+
runs-on: ubuntu-latest
20+
permissions:
21+
contents: read
22+
env:
23+
CDN_HOST: catalog.jan.ai # CDN domain pointing to S3 (CloudFront/Cloudflare/R2)
24+
BUCKET: ${{ secrets.CATALOG_AWS_S3_BUCKET_NAME }}
25+
AWS_REGION: ${{ secrets.CATALOG_AWS_REGION }}
26+
MAX_HISTORY: "20"
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Resolve release (automatic or manual)
32+
id: rel
33+
uses: actions/github-script@v7
34+
with:
35+
script: |
36+
const { owner, repo } = context.repo;
37+
const tagInput = core.getInput('tag_name');
38+
const nameInput = core.getInput('release_name');
39+
let release;
40+
41+
// Prefer tag_name if provided
42+
if (tagInput) {
43+
try {
44+
release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: tagInput });
45+
release = release.data;
46+
} catch (e) {
47+
core.setFailed(`Release not found for tag '${tagInput}'.`);
48+
return;
49+
}
50+
} else if (nameInput) {
51+
// GitHub does not support get-by-name directly → list and search
52+
const rels = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 });
53+
release = rels.find(r => (r.name || r.tag_name) === nameInput);
54+
if (!release) {
55+
core.setFailed(`Release not found for name '${nameInput}'.`);
56+
return;
57+
}
58+
} else {
59+
// Triggered by release event
60+
if (!context.payload.release) {
61+
core.setFailed('No release found in payload and no manual input provided.');
62+
return;
63+
}
64+
release = context.payload.release;
65+
}
66+
67+
const tag = release.tag_name;
68+
const name = release.name || tag;
69+
const assets = (release.assets || []).map(a => ({
70+
name: a.name,
71+
size: a.size,
72+
gh_url: a.browser_download_url
73+
}));
74+
75+
core.setOutput('tag', tag);
76+
core.setOutput('name', name);
77+
core.setOutput('published_at', release.published_at || new Date().toISOString());
78+
core.setOutput('assets', JSON.stringify(assets));
79+
80+
- name: Prepare assets directory
81+
run: mkdir -p out meta
82+
83+
- name: Download assets from GitHub
84+
run: |
85+
echo '${{ steps.rel.outputs.assets }}' \
86+
| jq -r '.[].gh_url' \
87+
| while read url; do
88+
f="out/$(basename "$url")"
89+
echo "Downloading $url -> $f"
90+
curl -L --fail -o "$f" "$url"
91+
done
92+
93+
- name: Build/merge releases.json (keep max 100 entries)
94+
env:
95+
TAG: ${{ steps.rel.outputs.tag }}
96+
NAME: ${{ steps.rel.outputs.name }}
97+
PUBLISHED_AT: ${{ steps.rel.outputs.published_at }}
98+
CDN_HOST: ${{ env.CDN_HOST }}
99+
MAX_HISTORY: ${{ env.MAX_HISTORY }}
100+
run: |
101+
test -f meta/releases.json || echo '[]' > meta/releases.json
102+
node - <<'NODE'
103+
const fs = require('fs')
104+
const path = require('path')
105+
106+
const TAG = process.env.TAG
107+
const NAME = process.env.NAME || TAG
108+
const PUBLISHED_AT = process.env.PUBLISHED_AT || new Date().toISOString()
109+
const CDN = process.env.CDN_HOST
110+
const MAX = parseInt(process.env.MAX_HISTORY || '100', 10)
111+
112+
const files = fs.readdirSync('out')
113+
const sizeOf = f => fs.statSync(path.join('out', f)).size
114+
115+
const entry = {
116+
tag_name: TAG,
117+
name: NAME,
118+
published_at: PUBLISHED_AT,
119+
assets: files.map(f => ({
120+
name: f,
121+
size: sizeOf(f),
122+
browser_download_url: `https://${CDN}/releases/${TAG}/${f}`
123+
}))
124+
}
125+
126+
const p = 'meta/releases.json'
127+
const arr = JSON.parse(fs.readFileSync(p, 'utf8'))
128+
129+
// Update or insert at the top
130+
const idx = arr.findIndex(x => x.tag_name === TAG)
131+
if (idx >= 0) arr.splice(idx, 1)
132+
arr.unshift(entry)
133+
134+
// Trim history to MAX entries
135+
while (arr.length > MAX) arr.pop()
136+
137+
fs.writeFileSync(p, JSON.stringify(arr, null, 2))
138+
NODE
139+
140+
- name: Configure AWS credentials
141+
uses: aws-actions/configure-aws-credentials@v4
142+
with:
143+
aws-access-key-id: ${{ secrets.CATALOG_AWS_ACCESS_KEY_ID }}
144+
aws-secret-access-key: ${{ secrets.CATALOG_AWS_SECRET_ACCESS_KEY }}
145+
aws-region: ${{ env.AWS_REGION }}
146+
147+
- name: Upload assets to S3
148+
env:
149+
TAG: ${{ steps.rel.outputs.tag }}
150+
BUCKET: ${{ env.BUCKET }}
151+
run: |
152+
echo "Uploading assets for tag $TAG to s3://$BUCKET/releases/$TAG/"
153+
aws s3 sync out "s3://$BUCKET/releases/$TAG/"
154+
155+
- name: Update 'latest' alias in S3
156+
env:
157+
TAG: ${{ steps.rel.outputs.tag }}
158+
BUCKET: ${{ env.BUCKET }}
159+
run: |
160+
echo "Updating 'latest' alias to point to tag $TAG"
161+
aws s3 rm "s3://$BUCKET/releases/latest/" --recursive || true
162+
aws s3 sync "s3://$BUCKET/releases/$TAG/" "s3://$BUCKET/releases/latest/"
163+
164+
- name: Upload releases.json
165+
env:
166+
BUCKET: ${{ env.BUCKET }}
167+
run: |
168+
echo "Uploading releases.json"
169+
aws s3 cp meta/releases.json "s3://$BUCKET/releases/releases.json" \
170+
--content-type 'application/json' --cache-control 'no-store'

0 commit comments

Comments
 (0)