|
| 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