diff --git a/.gitea/workflows/publish-pages.yml b/.gitea/workflows/publish-pages.yml index 01eff61..feaa83f 100644 --- a/.gitea/workflows/publish-pages.yml +++ b/.gitea/workflows/publish-pages.yml @@ -23,6 +23,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Show runtime versions + run: | + node --version + npm --version + - name: Resolve settings run: | SITE_NAME="${SITE_NAME_OVERRIDE:-${GITHUB_REPOSITORY##*/}}" @@ -57,13 +62,12 @@ jobs: ;; esac - - name: Install AWS CLI + - name: Install R2 publisher run: | - if ! command -v aws >/dev/null 2>&1; then - apt-get update - apt-get install -y --no-install-recommends awscli - rm -rf /var/lib/apt/lists/* - fi + PUBLISHER_DIR="${RUNNER_TEMP:-/tmp}/f40-pages-publisher" + mkdir -p "$PUBLISHER_DIR" + npm install --prefix "$PUBLISHER_DIR" --no-audit --no-fund @aws-sdk/client-s3 + echo "PUBLISHER_DIR=$PUBLISHER_DIR" >> "$GITHUB_ENV" - name: Build static site run: sh -c "$BUILD_COMMAND" @@ -82,22 +86,137 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.F40_PAGES_R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.F40_PAGES_R2_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: auto run: | RUN_ATTEMPT="${GITHUB_RUN_ATTEMPT:-1}" RELEASE="${GITHUB_SHA}-${RUN_ATTEMPT}" PREFIX="sites/${SITE_NAME}/releases/${RELEASE}" - PUBLISHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + export RELEASE PREFIX - aws s3 sync "$OUTPUT_DIR/" "s3://${R2_BUCKET}/${PREFIX}/" \ - --endpoint-url "$R2_ENDPOINT" \ - --delete \ - --cache-control "public,max-age=31536000,immutable" + cat > "$PUBLISHER_DIR/publish.mjs" <<'EOF' + import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; + import { createReadStream } from "node:fs"; + import { readdir, stat, writeFile } from "node:fs/promises"; + import { join, relative, sep } from "node:path"; - printf '{"site":"%s","release":"%s","sha":"%s","publishedAt":"%s"}\n' \ - "$SITE_NAME" "$RELEASE" "$GITHUB_SHA" "$PUBLISHED_AT" > current.json + const required = [ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "OUTPUT_DIR", + "PREFIX", + "R2_BUCKET", + "R2_ENDPOINT", + "RELEASE", + "GITHUB_SHA", + "SITE_NAME" + ]; - aws s3 cp current.json "s3://${R2_BUCKET}/sites/${SITE_NAME}/current.json" \ - --endpoint-url "$R2_ENDPOINT" \ - --content-type application/json \ - --cache-control no-store + for (const name of required) { + if (!process.env[name]) { + throw new Error(`${name} is required`); + } + } + + const client = new S3Client({ + endpoint: process.env.R2_ENDPOINT, + forcePathStyle: true, + region: "auto", + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } + }); + + const contentTypes = new Map([ + [".avif", "image/avif"], + [".css", "text/css; charset=utf-8"], + [".gif", "image/gif"], + [".html", "text/html; charset=utf-8"], + [".ico", "image/x-icon"], + [".jpeg", "image/jpeg"], + [".jpg", "image/jpeg"], + [".js", "text/javascript; charset=utf-8"], + [".json", "application/json; charset=utf-8"], + [".m4v", "video/mp4"], + [".mjs", "text/javascript; charset=utf-8"], + [".mov", "video/quicktime"], + [".mp4", "video/mp4"], + [".png", "image/png"], + [".svg", "image/svg+xml"], + [".txt", "text/plain; charset=utf-8"], + [".wasm", "application/wasm"], + [".webm", "video/webm"], + [".webp", "image/webp"], + [".xml", "application/xml; charset=utf-8"] + ]); + + async function walk(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await walk(path))); + } else if (entry.isFile()) { + files.push(path); + } + } + + return files; + } + + function contentTypeFor(path) { + const lower = path.toLowerCase(); + const index = lower.lastIndexOf("."); + if (index === -1) { + return "application/octet-stream"; + } + + return contentTypes.get(lower.slice(index)) ?? "application/octet-stream"; + } + + const outputDir = process.env.OUTPUT_DIR; + const files = await walk(outputDir); + + for (const file of files) { + const relativePath = relative(outputDir, file).split(sep).join("/"); + const key = `${process.env.PREFIX}/${relativePath}`; + const metadata = await stat(file); + + await client.send( + new PutObjectCommand({ + Bucket: process.env.R2_BUCKET, + Key: key, + Body: createReadStream(file), + ContentLength: metadata.size, + ContentType: contentTypeFor(file), + CacheControl: "public,max-age=31536000,immutable" + }) + ); + + console.log(`uploaded ${key}`); + } + + const current = { + site: process.env.SITE_NAME, + release: process.env.RELEASE, + sha: process.env.GITHUB_SHA, + publishedAt: new Date().toISOString() + }; + const currentPath = join(process.cwd(), "current.json"); + + await writeFile(currentPath, `${JSON.stringify(current)}\n`); + await client.send( + new PutObjectCommand({ + Bucket: process.env.R2_BUCKET, + Key: `sites/${process.env.SITE_NAME}/current.json`, + Body: createReadStream(currentPath), + ContentType: "application/json; charset=utf-8", + CacheControl: "no-store" + }) + ); + + console.log(`published ${process.env.SITE_NAME} release ${process.env.RELEASE}`); + EOF + + node "$PUBLISHER_DIR/publish.mjs"