diff --git a/.github/scripts/upload-posthog-js-s3.sh b/.github/scripts/upload-posthog-js-s3.sh new file mode 100755 index 0000000000..230258e979 --- /dev/null +++ b/.github/scripts/upload-posthog-js-s3.sh @@ -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 +# +# 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 }" +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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6397e7770..9abf2af283 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} @@ -334,11 +334,107 @@ jobs: message: '❌ Failed to release `${{ matrix.package.name }}@v${{ steps.check-package-version.outputs.committed-version }}`! ' emoji_reaction: 'x' + build-s3-artifacts: + 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! ' + 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