Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 122 additions & 9 deletions .github/workflows/static-passthrough.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
name: static-passthrough

# Reusable workflow for embed.kind = "static-html".
# Reusable workflow for embed.kind = "static-html" and friends.
#
# Uploads a directory tree of static files (HTML/JS/CSS/assets/...) into the
# project-embed assets bucket via the personal-site Worker's binding-proxy
# upload endpoint. Auth is a single shared bearer token; no R2 S3 keys
# anywhere.
# upload endpoint.
#
# Auth: GitHub Actions OIDC (primary). The job mints a short-lived ID token
# scoped to a fixed audience and presents it as the bearer; the Worker
# verifies the JWT signature against GitHub's JWKS and authorizes on
# `repository_owner`. No per-repo secrets needed.
#
# Fallback: if `secrets.PROJECT_EMBED_UPLOAD_TOKEN` is set on the calling
# repo, the workflow uses it instead. This keeps existing repos working
# while we roll OIDC out, and is also the right fit for any non-Actions
# uploader (manual scripts, etc.).
#
# Stored at: r2://assets-r2/<slug>/<git-sha>/<path>
# Served from: https://assets-r2.codeseys.io/<slug>/<git-sha>/<path>
#
# Calling repos invoke this with `secrets: inherit` so the workflow can
# read the user-level secret PROJECT_EMBED_UPLOAD_TOKEN.

on:
workflow_call:
Expand All @@ -36,10 +42,17 @@ on:
required: false
default: 'https://assets-r2.codeseys.io'
type: string
oidc-audience:
description: 'Audience the OIDC token will be minted for (must match the Worker)'
required: false
default: 'https://codeseys.io'
type: string

permissions:
contents: write
pull-requests: write
# Mint OIDC tokens. Required for the OIDC auth path.
id-token: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve static-token callers that omit id-token

Existing callers that follow this repo's documented reusable-workflow job permissions grant only contents: write and pull-requests: write; in GitHub's reusable-workflow permission model, the called workflow can only downgrade the caller job's token, so adding id-token: write here exceeds the caller's allowed permissions instead of merely making OIDC optional. Those runs fail before reaching the new fallback logic, so repos still using PROJECT_EMBED_UPLOAD_TOKEN are broken unless every caller is updated to include id-token: write.

Useful? React with 👍 / 👎.


jobs:
build-and-upload:
Expand Down Expand Up @@ -99,6 +112,53 @@ jobs:
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "schema_version=$SCHEMA_VERSION" >> $GITHUB_OUTPUT

- name: Install apt packages declared in manifest
env:
MANIFEST_PATH: ${{ inputs.manifest }}
run: |
set -euo pipefail
# Pull `build.aptPackages` from the manifest if present.
PKGS=$(jq -r '(.build.aptPackages // []) | join(" ")' "$MANIFEST_PATH")
if [ -z "$PKGS" ]; then
echo "No build.aptPackages in manifest; skipping apt install"
exit 0
fi
echo "Installing: $PKGS"
sudo apt-get update -qq
# shellcheck disable=SC2086
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $PKGS

- name: Run build.script (if declared)
env:
MANIFEST_PATH: ${{ inputs.manifest }}
SHORT_SHA: ${{ steps.manifest.outputs.short_sha }}
SLUG: ${{ steps.manifest.outputs.slug }}
run: |
set -euo pipefail
# `build.script` is an optional path to an executable that
# transforms source → upload-ready artifacts in-place under
# ${{ inputs.source-dir }}.
BUILD_SCRIPT=$(jq -r '.build.script // empty' "$MANIFEST_PATH")
if [ -z "$BUILD_SCRIPT" ]; then
echo "No build.script in manifest; skipping pre-upload transform"
exit 0
fi
if [ ! -f "$BUILD_SCRIPT" ]; then
echo "::error::build.script declared as '$BUILD_SCRIPT' but file not found"
exit 1
fi
if [ ! -x "$BUILD_SCRIPT" ]; then
echo "Marking $BUILD_SCRIPT as executable"
chmod +x "$BUILD_SCRIPT"
fi
echo "Running $BUILD_SCRIPT (source-dir=${{ inputs.source-dir }})"
# The script gets the manifest values as env vars in case it needs them.
export EMBED_SLUG="$SLUG"
export EMBED_VERSION="$SHORT_SHA"
export EMBED_SOURCE_DIR="${{ inputs.source-dir }}"
time "./$BUILD_SCRIPT"
echo "Build script complete; staged outputs ready for upload"

- name: Stage source directory
id: stage
run: |
Expand All @@ -121,19 +181,72 @@ jobs:
echo "bytes=$BYTES" >> $GITHUB_OUTPUT
echo "Staged $BYTES bytes ($FILES files) from ${{ inputs.source-dir }}"

- name: Mint OIDC token (primary auth path)
id: oidc
env:
AUDIENCE: ${{ inputs.oidc-audience }}
run: |
set -euo pipefail
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then
echo "::warning::OIDC env vars not present; skipping OIDC mint (falling back to static bearer)"
echo "have_oidc=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Mint a JWT scoped to AUDIENCE. Endpoint is the runner-provided
# token broker; the bearer is a one-time exchange token.
# See: https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect
TOKEN_RESP=$(curl -sS \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
-H "Accept: application/json" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${AUDIENCE}")
ID_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.value // empty')
if [ -z "$ID_TOKEN" ] || [ "$ID_TOKEN" = "null" ]; then
echo "::error::OIDC mint returned empty token. Response: $TOKEN_RESP"
exit 1
fi
# Don't echo the token; just confirm shape (3 segments) and length.
SEGS=$(echo "$ID_TOKEN" | tr '.' '\n' | wc -l)
LEN=${#ID_TOKEN}
if [ "$SEGS" -ne 3 ] || [ "$LEN" -lt 100 ]; then
echo "::error::OIDC token shape wrong (segments=$SEGS, length=$LEN)"
exit 1
fi
echo "have_oidc=true" >> "$GITHUB_OUTPUT"
# Pass token to subsequent steps via env (masked from logs by convention).
echo "::add-mask::$ID_TOKEN"
# Write to a step env file so the next step picks it up.
{
echo "OIDC_ID_TOKEN<<__OIDC_EOF__"
echo "$ID_TOKEN"
echo "__OIDC_EOF__"
} >> "$GITHUB_ENV"
echo "OIDC token minted: ${SEGS} segments, ${LEN} bytes"

- name: Upload via Worker binding-proxy
env:
UPLOAD_ENDPOINT: ${{ inputs.upload-endpoint }}
UPLOAD_TOKEN: ${{ secrets.PROJECT_EMBED_UPLOAD_TOKEN }}
HAVE_OIDC: ${{ steps.oidc.outputs.have_oidc }}
SLUG: ${{ steps.manifest.outputs.slug }}
VERSION: ${{ steps.manifest.outputs.short_sha }}
STAGE: ${{ steps.stage.outputs.stage_dir }}
run: |
set -euo pipefail
if [ -z "$UPLOAD_TOKEN" ]; then
echo "::error::PROJECT_EMBED_UPLOAD_TOKEN is not set; configure it as a user/org/repo secret"
# Pick the auth bearer: OIDC (if minted) wins; static fallback otherwise.
if [ "${HAVE_OIDC:-false}" = "true" ] && [ -n "${OIDC_ID_TOKEN:-}" ]; then
BEARER="$OIDC_ID_TOKEN"
AUTH_MODE="oidc"
elif [ -n "${UPLOAD_TOKEN:-}" ]; then
BEARER="$UPLOAD_TOKEN"
AUTH_MODE="bearer"
else
echo "::error::No auth available — neither OIDC nor PROJECT_EMBED_UPLOAD_TOKEN."
echo "::error::For OIDC: caller workflow must include 'permissions: id-token: write'."
echo "::error::For static fallback: set PROJECT_EMBED_UPLOAD_TOKEN as a repo secret."
exit 1
fi
echo "Authenticating via: ${AUTH_MODE}"

# Pre-flight health check
health=$(curl -s -o /dev/null -w '%{http_code}' "$UPLOAD_ENDPOINT")
if [ "$health" != "200" ]; then
Expand All @@ -151,7 +264,7 @@ jobs:
enc_rel=$(jq -nr --arg p "$rel" '$p|@uri')
url="${UPLOAD_ENDPOINT}?slug=${SLUG}&version=${VERSION}&path=${enc_rel}"
code=$(curl -sS -X PUT "$url" \
-H "Authorization: Bearer ${UPLOAD_TOKEN}" \
-H "Authorization: Bearer ${BEARER}" \
--data-binary "@${f}" \
-o /tmp/upload-resp.json \
-w '%{http_code}')
Expand Down