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
84 changes: 75 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 Avoid elevating id-token in the called workflow

For existing callers that already set a job permissions block without id-token: write (the README example does this), GitHub treats unspecified permissions as none and a reusable workflow can only maintain or reduce the caller's token permissions, not elevate them. Because this called workflow now requests id-token: write, those callers are rejected before any step runs, so the new static-token fallback never executes even when PROJECT_EMBED_UPLOAD_TOKEN is present. This makes the rollout breaking for exactly the “caller forgot id-token” case described in the change.

Useful? React with 👍 / 👎.


jobs:
build-and-upload:
Expand Down Expand Up @@ -121,19 +134,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 +217,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