Skip to content
Merged
75 changes: 75 additions & 0 deletions .github/scripts/upload-posthog-js-s3.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
#
# Upload posthog-js dist artifacts to S3 and append the version to versions.json.
#
# Usage:
# VERSION=1.365.0 ./upload-posthog-js-s3.sh <bucket>
#
# VERSION must be set as an environment variable (not an argument) to avoid
# shell injection if the value were ever attacker-influenced.
#
# Expects AWS credentials to be configured before invocation.
#
set -euo pipefail

BUCKET="${1:?Usage: VERSION=x.y.z $0 <bucket>}"
DIST_DIR="packages/browser/dist"

if [[ -z "${VERSION:-}" ]]; then
echo "ERROR: VERSION environment variable is required" >&2
exit 1
fi

# Validate version is strict semver (e.g. 1.365.0 or 1.365.0-beta.1).
# Prevents path traversal — no slashes, dots only in expected positions.
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-][a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Invalid version format: '$VERSION'" >&2
exit 1
fi

echo "==> Uploading posthog-js v$VERSION to s3://$BUCKET/$VERSION/"
aws s3 cp "$DIST_DIR/" "s3://$BUCKET/$VERSION/" \
--recursive \
--exclude "*" \
--include "*.js" \
--cache-control "public, max-age=31536000, immutable" \
--content-type "application/javascript"

echo "==> Updating versions.json in s3://$BUCKET/"
TMPWORKDIR="$(mktemp -d)"
trap 'rm -rf "$TMPWORKDIR"' EXIT

# Distinguish "file doesn't exist" from real errors (auth, network).
# A blind fallback to '[]' on any error would silently drop all previous versions.
if aws s3 cp "s3://$BUCKET/versions.json" "$TMPWORKDIR/versions.json"; then
echo "Downloaded existing versions.json"
elif aws s3api head-object --bucket "$BUCKET" --key "versions.json" 2>/dev/null; then
echo "ERROR: versions.json exists but could not be downloaded" >&2
exit 1
else
echo "No existing versions.json found, starting fresh"
echo '[]' > "$TMPWORKDIR/versions.json"
fi

if jq -e --arg v "$VERSION" '.[] | select(.version == $v)' "$TMPWORKDIR/versions.json" > /dev/null 2>&1; then
echo "Version $VERSION already in versions.json, skipping"
else
jq --arg v "$VERSION" --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'. + [{"version": $v, "timestamp": $ts}]' "$TMPWORKDIR/versions.json" > "$TMPWORKDIR/versions_updated.json"

# Validate the updated manifest before uploading: must be a non-empty JSON array
# where every entry has .version and .timestamp strings, and length is exactly original + 1.
EXPECTED_LENGTH=$(( $(jq 'length' "$TMPWORKDIR/versions.json") + 1 ))
if ! jq -e --argjson expected "$EXPECTED_LENGTH" 'if type != "array" then error
elif length != $expected then error
elif any(.[]; (.version | type) != "string" or (.timestamp | type) != "string") then error
else true end' "$TMPWORKDIR/versions_updated.json" > /dev/null 2>&1; then
echo "ERROR: versions_updated.json failed validation — aborting upload" >&2
cat "$TMPWORKDIR/versions_updated.json" >&2
exit 1
fi

aws s3 cp "$TMPWORKDIR/versions_updated.json" "s3://$BUCKET/versions.json" \
--content-type "application/json"
echo "Added v$VERSION to versions.json"
fi
102 changes: 99 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ jobs:

- name: Check ${{ matrix.package.name }} version and detect an update
id: check-package-version
uses: PostHog/check-package-version@v2.1.0
uses: PostHog/check-package-version@v2
with:
path: ${{ steps.get-package-path.outputs.path }}

Expand Down Expand Up @@ -334,11 +334,107 @@ jobs:
message: '❌ Failed to release `${{ matrix.package.name }}@v${{ steps.check-package-version.outputs.committed-version }}`! <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>'
emoji_reaction: 'x'

build-s3-artifacts:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The aws script is racey, so verified this workflow already limits concurrency to 1 👍

name: Build posthog-js dist for S3
needs: [version-bump, publish, notify-approval-needed]
runs-on: ubuntu-latest
# Run as long as the version bump committed — even if some matrix publishes failed,
# posthog-js might have succeeded and we still want the artifacts in S3.
if: always() && needs.version-bump.outputs.commit-hash != ''
permissions:
contents: read
outputs:
is-new-version: ${{ steps.check-version.outputs.is-new-version }}
committed-version: ${{ steps.check-version.outputs.committed-version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ needs.version-bump.outputs.commit-hash }}
fetch-depth: 1

- name: Check posthog-js version
id: check-version
uses: PostHog/check-package-version@v2
with:
path: packages/browser

- name: Setup environment
if: steps.check-version.outputs.is-new-version == 'true'
uses: ./.github/actions/setup

- name: Upload dist artifact
if: steps.check-version.outputs.is-new-version == 'true'
uses: actions/upload-artifact@v4
with:
name: posthog-js-dist
path: packages/browser/dist/*.js
retention-days: 1
if-no-files-found: error

upload-s3:
name: Upload posthog-js dist to S3
needs: [build-s3-artifacts, version-bump, notify-approval-needed]
runs-on: ubuntu-latest
if: always() && needs.build-s3-artifacts.outputs.is-new-version == 'true'
environment: 'S3 Upload' # For OIDC credential scoping only — no required reviewers (single approval at NPM Release)
permissions:
contents: read
id-token: write
steps:
# Sparse checkout: only the upload script — no pnpm install/build runs here.
- name: Checkout upload script
uses: actions/checkout@v6
with:
ref: ${{ needs.version-bump.outputs.commit-hash }}
sparse-checkout: .github/scripts

- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: posthog-js-dist
path: packages/browser/dist

# Upload to US (us-east-1)
- name: Configure AWS credentials (US)
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
with:
role-to-assume: ${{ vars.AWS_S3_UPLOAD_ROLE_ARN_US }}
aws-region: us-east-1

- name: Upload dist and update manifest (US)
env:
VERSION: ${{ needs.build-s3-artifacts.outputs.committed-version }}
run: .github/scripts/upload-posthog-js-s3.sh posthog-js-prod-us

# Upload to EU (eu-central-1)
- name: Configure AWS credentials (EU)
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
with:
role-to-assume: ${{ vars.AWS_S3_UPLOAD_ROLE_ARN_EU }}
aws-region: eu-central-1

- name: Upload dist and update manifest (EU)
env:
VERSION: ${{ needs.build-s3-artifacts.outputs.committed-version }}
run: .github/scripts/upload-posthog-js-s3.sh posthog-js-prod-eu

- name: Notify Slack - S3 Upload Failed
continue-on-error: true
if: ${{ failure() && needs.notify-approval-needed.outputs.slack_ts != '' }}
uses: PostHog/.github/.github/actions/slack-thread-reply@main
with:
slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }}
slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }}
thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }}
message: '❌ Failed to upload posthog-js v${{ needs.build-s3-artifacts.outputs.committed-version }} dist to S3! <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>'
emoji_reaction: 'x'

notify-released:
name: Notify Slack - Released
needs: [notify-approval-needed, publish]
needs: [notify-approval-needed, publish, upload-s3]
runs-on: ubuntu-latest
if: always() && needs.publish.result == 'success' && needs.notify-approval-needed.outputs.slack_ts != ''
if: always() && needs.publish.result == 'success' && (needs.upload-s3.result == 'success' || needs.upload-s3.result == 'skipped') && needs.notify-approval-needed.outputs.slack_ts != ''
steps:
- name: Checkout repository
uses: actions/checkout@v6
Expand Down
Loading