Skip to content
Open
62 changes: 62 additions & 0 deletions .github/scripts/upload-posthog-js-s3.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/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/"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' 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" "$TMPDIR/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 '[]' > "$TMPDIR/versions.json"
fi

if jq -e --arg v "$VERSION" '.[] | select(.version == $v)' "$TMPDIR/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}]' "$TMPDIR/versions.json" > "$TMPDIR/versions_updated.json"
aws s3 cp "$TMPDIR/versions_updated.json" "s3://$BUCKET/versions.json" \
--content-type "application/json"
Comment on lines +57 to +73
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.

This seems like the riskiest bit to me, since one bad change will break the entire file. I'm assuming using jq to modify the file means it will only ever generate valid json. Is there any additional check we can run here to verify the contents match what we expect before publishing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've added some additional verification's using jq:

  • It's a JSON array
  • The length of the array is exactly original + 1
  • Every entry is of the expected schema

This is a good point though. Breaking this file would mean breaking version synchronization. I'll make a note to add some server-side observability for this case.

echo "Added v$VERSION to versions.json"
fi
100 changes: 98 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
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: 0

- name: Check posthog-js version
id: check-version
uses: PostHog/check-package-version@v2.1.0
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.notify-approval-needed.outputs.slack_ts != ''
steps:
- name: Checkout repository
uses: actions/checkout@v6
Expand Down
Loading