Skip to content

Upload R2

Upload R2 #10

Workflow file for this run

name: Upload R2
on:
workflow_run:
workflows:
- Release
types:
- completed
workflow_dispatch:
inputs:
tag:
description: GitHub Release tag to upload to R2, for example v0.4.8
required: true
type: string
permissions:
actions: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.run_id }}
cancel-in-progress: false
jobs:
upload:
name: Upload release assets to R2
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Resolve R2 public base URL
id: r2_config
env:
ONETCLI_PUBLIC_BASE_URL: ${{ vars.ONETCLI_PUBLIC_BASE_URL }}
run: |
base_url="${ONETCLI_PUBLIC_BASE_URL%/}"
if [ -z "$base_url" ]; then
echo "::warning::ONETCLI_PUBLIC_BASE_URL repo variable is not set; skipping R2 upload. GitHub Release remains the fallback source."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "base_url=$base_url" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Resolve release tag
id: release
if: ${{ steps.r2_config.outputs.skip != 'true' }}
env:
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || '' }}
WORKFLOW_RUN_ID: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || '' }}
run: |
set -euo pipefail
if [ -n "$INPUT_TAG" ]; then
release_tag="$INPUT_TAG"
else
mkdir -p run-metadata
if ! gh run download "$WORKFLOW_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--name release-metadata \
--dir run-metadata; then
echo "::warning::Release workflow metadata was not found; skipping R2 upload."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
release_tag="$(
node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync("run-metadata/release-metadata.json", "utf8")); process.stdout.write(data.release_tag || "");'
)"
fi
if [ -z "$release_tag" ]; then
echo "::warning::Release tag is empty; skipping R2 upload."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
gh release view "$release_tag" --repo "$GITHUB_REPOSITORY" >/dev/null
echo "tag=$release_tag" >> "$GITHUB_OUTPUT"
echo "version=${release_tag#v}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Show R2 upload target
if: ${{ steps.r2_config.outputs.skip != 'true' && steps.release.outputs.skip != 'true' }}
run: |
echo "Release tag: ${{ steps.release.outputs.tag }}"
echo "R2 public base URL: ${{ steps.r2_config.outputs.base_url }}"
- name: Verify R2 secrets are set
if: ${{ steps.r2_config.outputs.skip != 'true' && steps.release.outputs.skip != 'true' }}
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_R2_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
CLOUDFLARE_R2_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
run: |
for name in CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_R2_ACCESS_KEY_ID CLOUDFLARE_R2_SECRET_ACCESS_KEY CLOUDFLARE_R2_BUCKET; do
if [ -z "${!name}" ]; then
echo "::error::${name} secret is empty or not set"
exit 1
fi
done
- name: Download GitHub Release assets
if: ${{ steps.r2_config.outputs.skip != 'true' && steps.release.outputs.skip != 'true' }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
mkdir -p artifacts
gh release download "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern "onetcli-aarch64-apple-darwin.tar.gz" \
--pattern "onetcli-x86_64-apple-darwin.tar.gz" \
--pattern "onetcli-x86_64-unknown-linux-gnu.tar.gz" \
--pattern "onetcli-aarch64-unknown-linux-gnu.tar.gz" \
--pattern "onetcli-x86_64-pc-windows-msvc.zip" \
--pattern "onetcli-aarch64-apple-darwin.dmg" \
--pattern "onetcli-x86_64-apple-darwin.dmg" \
--pattern "sha256sums.txt" \
--dir artifacts
ls -la artifacts
- name: Verify downloaded release assets
if: ${{ steps.r2_config.outputs.skip != 'true' && steps.release.outputs.skip != 'true' }}
run: |
set -euo pipefail
for file in \
onetcli-aarch64-apple-darwin.tar.gz \
onetcli-x86_64-apple-darwin.tar.gz \
onetcli-x86_64-unknown-linux-gnu.tar.gz \
onetcli-aarch64-unknown-linux-gnu.tar.gz \
onetcli-x86_64-pc-windows-msvc.zip \
onetcli-aarch64-apple-darwin.dmg \
onetcli-x86_64-apple-darwin.dmg \
sha256sums.txt; do
if [ ! -s "artifacts/${file}" ]; then
echo "::error::Missing or empty GitHub Release asset: ${file}"
exit 1
fi
done
for file in \
onetcli-aarch64-apple-darwin.tar.gz \
onetcli-x86_64-apple-darwin.tar.gz \
onetcli-x86_64-unknown-linux-gnu.tar.gz \
onetcli-aarch64-unknown-linux-gnu.tar.gz \
onetcli-x86_64-pc-windows-msvc.zip \
onetcli-aarch64-apple-darwin.dmg \
onetcli-x86_64-apple-darwin.dmg; do
if ! awk -v file="$file" '$2 == file && $1 ~ /^[0-9a-f]{64}$/ { found = 1 } END { exit found ? 0 : 1 }' artifacts/sha256sums.txt; then
echo "::error::sha256sums.txt is missing checksum for ${file}"
exit 1
fi
done
- name: Generate R2 update manifest
if: ${{ steps.r2_config.outputs.skip != 'true' && steps.release.outputs.skip != 'true' }}
env:
ONETCLI_PUBLIC_BASE_URL: ${{ steps.r2_config.outputs.base_url }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
node <<'NODE'
const fs = require("fs");
const baseUrl = process.env.ONETCLI_PUBLIC_BASE_URL.replace(/\/+$/, "");
const releaseTag = process.env.RELEASE_TAG;
const version = process.env.RELEASE_VERSION;
const repository = process.env.GITHUB_REPOSITORY;
if (!repository) throw new Error("GITHUB_REPOSITORY is required");
const checksums = new Map(
fs
.readFileSync("artifacts/sha256sums.txt", "utf8")
.trim()
.split(/\n/)
.filter(Boolean)
.map((line) => {
const [sha256, fileName] = line.trim().split(/\s+/, 2);
return [fileName, sha256];
}),
);
const appAssets = {
"aarch64-apple-darwin": "onetcli-aarch64-apple-darwin.tar.gz",
"x86_64-apple-darwin": "onetcli-x86_64-apple-darwin.tar.gz",
"x86_64-unknown-linux-gnu": "onetcli-x86_64-unknown-linux-gnu.tar.gz",
"aarch64-unknown-linux-gnu": "onetcli-aarch64-unknown-linux-gnu.tar.gz",
"x86_64-pc-windows-msvc": "onetcli-x86_64-pc-windows-msvc.zip",
};
function checksumFor(fileName) {
const sha256 = checksums.get(fileName);
if (!sha256) throw new Error(`missing checksum for ${fileName}`);
return sha256;
}
function objectUrls(prefix, assets) {
return Object.fromEntries(
Object.entries(assets).map(([target, fileName]) => [
target,
`${baseUrl}/${prefix}/${releaseTag}/${fileName}`,
]),
);
}
function objectChecksums(assets) {
return Object.fromEntries(
Object.entries(assets).map(([target, fileName]) => [
target,
checksumFor(fileName),
]),
);
}
function githubReleaseUrls(assets) {
return Object.fromEntries(
Object.entries(assets).map(([target, fileName]) => [
target,
`https://github.com/${repository}/releases/download/${releaseTag}/${fileName}`,
]),
);
}
fs.mkdirSync("artifacts/updates", { recursive: true });
fs.writeFileSync(
"artifacts/updates/latest.json",
`${JSON.stringify(
{
schema_version: 1,
version,
release_page_url: `${baseUrl}/releases/${releaseTag}/`,
downloads: objectUrls("releases", appAssets),
fallback_downloads: githubReleaseUrls(appAssets),
sha256s: objectChecksums(appAssets),
},
null,
2,
)}\n`,
);
NODE
- name: Upload release assets and update manifest to R2
if: ${{ steps.r2_config.outputs.skip != 'true' && steps.release.outputs.skip != 'true' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
AWS_EC2_METADATA_DISABLED: true
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
endpoint="https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com"
upload_object() {
local source="$1"
local key="$2"
local content_type="$3"
local cache_control="$4"
aws s3 cp "$source" "s3://${CLOUDFLARE_R2_BUCKET}/${key}" \
--endpoint-url "$endpoint" \
--content-type "$content_type" \
--cache-control "$cache_control"
}
for file in \
onetcli-aarch64-apple-darwin.tar.gz \
onetcli-x86_64-apple-darwin.tar.gz \
onetcli-x86_64-unknown-linux-gnu.tar.gz \
onetcli-aarch64-unknown-linux-gnu.tar.gz \
onetcli-x86_64-pc-windows-msvc.zip \
onetcli-aarch64-apple-darwin.dmg \
onetcli-x86_64-apple-darwin.dmg \
sha256sums.txt; do
if [[ "$file" == *.zip ]]; then
content_type="application/zip"
elif [[ "$file" == *.dmg ]]; then
content_type="application/x-apple-diskimage"
elif [[ "$file" == *.txt ]]; then
content_type="text/plain; charset=utf-8"
else
content_type="application/gzip"
fi
upload_object "artifacts/${file}" "releases/${RELEASE_TAG}/${file}" "$content_type" "public, max-age=31536000, immutable"
done
upload_object "artifacts/updates/latest.json" "updates/latest.json" "application/json; charset=utf-8" "no-cache"