chore: test workflow #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Mirror Releases to S3 | |
| on: | |
| release: | |
| types: [published] # Auto trigger when a release is published | |
| workflow_dispatch: # Manual trigger | |
| inputs: | |
| tag_name: | |
| description: "Release tag (e.g. v0.6.9). Takes precedence if both provided." | |
| required: false | |
| type: string | |
| release_name: | |
| description: "Release name (e.g. Jan 0.6.9). Used when tag_name is not provided." | |
| required: false | |
| type: string | |
| push: # Auto trigger on push for a specific test branch | |
| branches: | |
| - feat/release-to-aws-s3 # ← change to your PR branch name | |
| jobs: | |
| mirror: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| CDN_HOST: catalog.jan.ai # CDN domain pointing to S3 (CloudFront/Cloudflare/R2) | |
| BUCKET: ${{ secrets.CATALOG_AWS_S3_BUCKET_NAME }} | |
| AWS_REGION: ${{ secrets.CATALOG_AWS_REGION }} | |
| MAX_HISTORY: "20" | |
| # Hardcoded test values used only on push (optional) | |
| TEST_TAG: b4951 # ← put a real existing tag to test | |
| TEST_NAME: "" # or a release name if you prefer to match by name | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Resolve release (automatic/manual/push-test) | |
| id: rel | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| // Inputs from workflow_dispatch | |
| const tagInput = core.getInput('tag_name'); | |
| const nameInput = core.getInput('release_name'); | |
| // Env for push-test | |
| const testTag = process.env.TEST_TAG && process.env.TEST_TAG.trim() ? process.env.TEST_TAG.trim() : ''; | |
| const testName = process.env.TEST_NAME && process.env.TEST_NAME.trim() ? process.env.TEST_NAME.trim() : ''; | |
| // Resolution order: | |
| // 1) tag_name input | |
| // 2) TEST_TAG env (for push test) | |
| // 3) release_name input | |
| // 4) TEST_NAME env (for push test) | |
| // 5) context.payload.release (release event) | |
| let release = null; | |
| let chosen = null; | |
| async function getByTag(tag) { | |
| const r = await github.rest.repos.getReleaseByTag({ owner, repo, tag }); | |
| return r.data; | |
| } | |
| async function getByName(name) { | |
| const rels = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 }); | |
| return rels.find(r => (r.name || r.tag_name) === name) || null; | |
| } | |
| try { | |
| if (tagInput) { | |
| chosen = `tag_input:${tagInput}`; | |
| release = await getByTag(tagInput); | |
| } else if (testTag) { | |
| chosen = `test_tag:${testTag}`; | |
| release = await getByTag(testTag); | |
| } else if (nameInput) { | |
| chosen = `name_input:${nameInput}`; | |
| release = await getByName(nameInput); | |
| } else if (testName) { | |
| chosen = `test_name:${testName}`; | |
| release = await getByName(testName); | |
| } else if (context.payload.release) { | |
| chosen = 'payload_release_event'; | |
| release = context.payload.release; | |
| } else { | |
| core.setFailed('No release reference provided: set tag_name, TEST_TAG, release_name, TEST_NAME, or trigger from a release event.'); | |
| return; | |
| } | |
| } catch (e) { | |
| core.setFailed(`Failed to resolve release (${chosen}). ${e.message}`); | |
| return; | |
| } | |
| if (!release) { | |
| core.setFailed(`Release not found (${chosen}).`); | |
| return; | |
| } | |
| const tag = release.tag_name; | |
| const name = release.name || tag; | |
| const assets = (release.assets || []).map(a => ({ | |
| name: a.name, | |
| size: a.size, | |
| gh_url: a.browser_download_url | |
| })); | |
| core.info(`Resolved release via ${chosen}: tag=${tag}, name=${name}, assets=${assets.length}`); | |
| core.setOutput('tag', tag); | |
| core.setOutput('name', name); | |
| core.setOutput('published_at', release.published_at || new Date().toISOString()); | |
| core.setOutput('assets', JSON.stringify(assets)); | |
| - name: Prepare assets directory | |
| run: mkdir -p out meta | |
| - name: Download assets from GitHub | |
| run: | | |
| echo '${{ steps.rel.outputs.assets }}' \ | |
| | jq -r '.[].gh_url' \ | |
| | while read url; do | |
| f="out/$(basename "$url")" | |
| echo "Downloading $url -> $f" | |
| curl -L --fail -o "$f" "$url" | |
| done | |
| - name: Build/merge releases.json (keep max N entries) | |
| env: | |
| TAG: ${{ steps.rel.outputs.tag }} | |
| NAME: ${{ steps.rel.outputs.name }} | |
| PUBLISHED_AT: ${{ steps.rel.outputs.published_at }} | |
| CDN_HOST: ${{ env.CDN_HOST }} | |
| MAX_HISTORY: ${{ env.MAX_HISTORY }} | |
| run: | | |
| test -f meta/releases.json || echo '[]' > meta/releases.json | |
| node - <<'NODE' | |
| const fs = require('fs') | |
| const path = require('path') | |
| const TAG = process.env.TAG | |
| const NAME = process.env.NAME || TAG | |
| const PUBLISHED_AT = process.env.PUBLISHED_AT || new Date().toISOString() | |
| const CDN = process.env.CDN_HOST | |
| const MAX = parseInt(process.env.MAX_HISTORY || '20', 10) | |
| const files = fs.readdirSync('out') | |
| const sizeOf = f => fs.statSync(path.join('out', f)).size | |
| const entry = { | |
| tag_name: TAG, | |
| name: NAME, | |
| published_at: PUBLISHED_AT, | |
| assets: files.map(f => ({ | |
| name: f, | |
| size: sizeOf(f), | |
| browser_download_url: `https://${CDN}/releases/${TAG}/${f}` | |
| })) | |
| } | |
| const p = 'meta/releases.json' | |
| const arr = JSON.parse(fs.readFileSync(p, 'utf8')) | |
| // Update or insert at the top | |
| const idx = arr.findIndex(x => x.tag_name === TAG) | |
| if (idx >= 0) arr.splice(idx, 1) | |
| arr.unshift(entry) | |
| // Trim history to MAX entries | |
| while (arr.length > MAX) arr.pop() | |
| fs.writeFileSync(p, JSON.stringify(arr, null, 2)) | |
| NODE | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.CATALOG_AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.CATALOG_AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Upload assets to S3 | |
| env: | |
| TAG: ${{ steps.rel.outputs.tag }} | |
| BUCKET: ${{ env.BUCKET }} | |
| run: | | |
| echo "Uploading assets for tag $TAG to s3://$BUCKET/releases/$TAG/" | |
| aws s3 sync out "s3://$BUCKET/releases/$TAG/" | |
| - name: Update 'latest' alias in S3 | |
| env: | |
| TAG: ${{ steps.rel.outputs.tag }} | |
| BUCKET: ${{ env.BUCKET }} | |
| run: | | |
| echo "Updating 'latest' alias to point to tag $TAG" | |
| aws s3 rm "s3://$BUCKET/releases/latest/" --recursive || true | |
| aws s3 sync "s3://$BUCKET/releases/$TAG/" "s3://$BUCKET/releases/latest/" | |
| - name: Upload releases.json | |
| env: | |
| BUCKET: ${{ env.BUCKET }} | |
| run: | | |
| echo "Uploading releases.json" | |
| aws s3 cp meta/releases.json "s3://$BUCKET/releases/releases.json" \ | |
| --content-type 'application/json' --cache-control 'no-store' |