diff --git a/.gitea/workflows/mirror.yml b/.gitea/workflows/mirror.yml index 23e527a4..650d8e4a 100644 --- a/.gitea/workflows/mirror.yml +++ b/.gitea/workflows/mirror.yml @@ -4,6 +4,10 @@ on: push: branches: [master] tags: ["*"] + # A release goes public whenever its last artifact lands, which can be from a local run hours + # after the tag, so its copy on GitHub is kept up to date by polling + schedule: + - cron: "*/30 * * * *" workflow_dispatch: # Serialized rather than cancel-in-progress: aborting a push halfway leaves the @@ -14,7 +18,7 @@ concurrency: jobs: push-to-github: runs-on: ubuntu-latest - if: github.repository == 'coracle/flotilla' + if: github.repository == 'coracle/flotilla' && github.event_name != 'schedule' steps: - name: Push master and tags to GitHub @@ -32,3 +36,24 @@ jobs: "https://x-access-token:$GH_MIRROR_TOKEN@github.com/coracle-social/flotilla.git" \ 'refs/heads/master:refs/heads/master' \ 'refs/tags/*:refs/tags/*' + + # Obtainium reads releases from whichever url a user added, and many added the GitHub one + copy-release-to-github: + runs-on: ubuntu-latest + needs: push-to-github + if: ${{ !cancelled() && github.repository == 'coracle/flotilla' && needs.push-to-github.result != 'failure' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Copy gitea's latest release to GitHub + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_MIRROR_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} + run: node scripts/release/github.mjs diff --git a/scripts/release/github.mjs b/scripts/release/github.mjs new file mode 100644 index 00000000..d7d565a3 --- /dev/null +++ b/scripts/release/github.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +// Copies gitea's latest published release to the GitHub mirror, whose releases are what Obtainium +// sees for anyone who added the app by its GitHub url. Run by .gitea/workflows/mirror.yml. +import {gitea} from "./lib/gitea.mjs" +import {fail} from "./lib/shell.mjs" + +const mirror = "coracle-social/flotilla" +const repository = new URL(`${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`) +const token = process.env.GH_MIRROR_TOKEN + +if (!token || !process.env.GITEA_TOKEN) { + fail("Set GITEA_TOKEN and GH_MIRROR_TOKEN") +} + +const github = async (method, url, {body, allow404} = {}) => { + const binary = body instanceof Uint8Array + const response = await fetch(url.startsWith("https://") ? url : `https://api.github.com${url}`, { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + ...(body ? {"Content-Type": binary ? "application/octet-stream" : "application/json"} : {}), + }, + body: binary ? body : body && JSON.stringify(body), + }) + + if (response.status === 404 && allow404) { + return undefined + } + + if (!response.ok) { + throw new Error(`${method} ${url} responded ${response.status}: ${await response.text()}`) + } + + return response.status === 204 ? undefined : response.json() +} + +const source = await gitea({repository, token: process.env.GITEA_TOKEN}).latestRelease() + +if (!source) { + console.log("gitea has no published release") + process.exit(0) +} + +const tag = source.tag_name + +// Creating a release for a tag GitHub doesn't have yet would tag its default branch instead +if (!(await github("GET", `/repos/${mirror}/git/ref/tags/${tag}`, {allow404: true}))) { + fail(`${mirror} has no ${tag} tag yet; the mirror job pushes it`) +} + +const release = + (await github("GET", `/repos/${mirror}/releases/tags/${tag}`, {allow404: true})) ?? + (await github("POST", `/repos/${mirror}/releases`, { + body: {tag_name: tag, name: source.name, body: source.body, make_latest: "true"}, + })) + +for (const asset of source.assets) { + const existing = release.assets.find(({name}) => name === asset.name) + + // A moved tag rebuilds its assets under the same names, which only the size gives away + if (existing?.size === asset.size) { + continue + } + + if (existing) { + await github("DELETE", `/repos/${mirror}/releases/assets/${existing.id}`) + } + + const response = await fetch(asset.browser_download_url) + + if (!response.ok) { + throw new Error(`Downloading ${asset.name} responded ${response.status}`) + } + + await github( + "POST", + `https://uploads.github.com/repos/${mirror}/releases/${release.id}/assets?name=${encodeURIComponent(asset.name)}`, + {body: new Uint8Array(await response.arrayBuffer())}, + ) + + console.log(`${tag}: ${asset.name}`) +} diff --git a/scripts/release/lib/gitea.mjs b/scripts/release/lib/gitea.mjs index 5b4bd1e2..3f60ecbb 100644 --- a/scripts/release/lib/gitea.mjs +++ b/scripts/release/lib/gitea.mjs @@ -26,6 +26,8 @@ export const gitea = ({repository, token}) => { return { hasTag: async tag => Boolean(await api("GET", `/tags/${tag}`, {allow404: true})), + latestRelease: () => api("GET", "/releases/latest", {allow404: true}), + upsertRelease: async (tag, notes) => (await api("GET", `/releases/tags/${tag}`, {allow404: true})) ?? (await api("POST", "/releases", {