Skip to content

Instantly share code, notes, and snippets.

@byrnehollander
Created February 14, 2023 19:02
Show Gist options
  • Save byrnehollander/3002999a6bf00fd286ea5206e8de823c to your computer and use it in GitHub Desktop.
Save byrnehollander/3002999a6bf00fd286ea5206e8de823c to your computer and use it in GitHub Desktop.
Upload Sentry source maps on `eas update`

expo-sentry-sourcemaps-update

Requirements

  • This requires Expo SDK 47, with sentry-expo@6.0.0 or above and expo-updates@0.15.5 or above.
  • You should also install @babel/cli and @sentry/cli
  • See Expo docs
  1. Configure the sentry-expo/upload-sourcemaps postPublish hook in app.json or app.config.ts. Expo docs. The script will retrieve your Sentry auth config through theses props.
{
  "hooks": {
      "postPublish": [
        {
          "file": "sentry-expo/upload-sourcemaps",
          "config": {
            "organization": "my-sentry-org",
            "project": "my-project-name",
            "authToken": "<snip>"
          }
        }
      ]
    }
}

2. Copy `upload_sourcemaps_to_sentry_es6.js` to `.github/workflows`

  1. When you run eas update, pipe the output to output.txt
eas update --non-interactive --branch production --message "Some message" | tee output.txt

Example

deploy_ota:
  steps:
    - name: Check for EXPO_TOKEN
      run: |
        if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
          echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
          exit 1
        fi

    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Setup Node
      uses: actions/setup-node@v3
      with:
        node-version: 16.x
        cache: yarn

    - name: Setup Expo
      uses: expo/expo-github-action@v7
      with:
        expo-version: 6.2.1
        eas-version: 3.5.2
        token: ${{ secrets.EXPO_TOKEN }}

    - name: Find yarn cache
      id: yarn-cache-path
      run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT

    - name: Restore cache
      uses: actions/cache@v3
      with:
        path: ${{ steps.yarn-cache-path.outputs.dir }}
        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
        restore-keys: ${{ runner.os }}-yarn-

    - name: Install app dependencies
      run: yarn install --immutable

    - name: Publish OTA update
      run: |
        eas update --non-interactive --branch production --message "New production update" | tee output.txt

    - name: Transpile upload_sourcemaps_to_sentry
      shell: bash
      run: yarn babel .github/workflows/upload_sourcemaps_to_sentry_es6.js --out-file .github/workflows/upload_sourcemaps_to_sentry.js

    - name: Upload sourcemaps to Sentry
      shell: bash
      env:
        SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      run: node .github/workflows/upload_sourcemaps_to_sentry.js

Contributions

This is adapated from @ajacquierbret's comment in this Expo issue.

import { getConfig } from "@expo/config";
import spawnAsync from "@expo/spawn-async";
import fs from "fs";
import path from "path";
const findInOutput = (output, searchEl) =>
output
.find((line) => line.toLowerCase().startsWith(searchEl))
?.split(" ")
.pop();
const findUpdateId = (output, platform) =>
findInOutput(output, `${platform} update id`);
const getBundles = () => {
const bundles = fs.readdirSync("dist/bundles");
const iosBundle = bundles.find(
(s) => s.startsWith("ios-") && s.endsWith(".js")
);
const iosMap = bundles.find(
(s) => s.startsWith("ios-") && s.endsWith(".map")
);
const androidBundle = bundles.find(
(s) => s.startsWith("android-") && s.endsWith(".js")
);
const androidMap = bundles.find(
(s) => s.startsWith("android-") && s.endsWith(".map")
);
return { iosBundle, iosMap, androidBundle, androidMap };
};
const uploadSourcemapsAsync = async ({
iosUpdateId,
iosMap,
androidUpdateId,
androidMap,
}) => {
const expoConfig = getConfig(path.resolve(__dirname, "../../")).exp;
const version = expoConfig.version;
const iOSConfig = {
bundleIdentifier: expoConfig.ios?.bundleIdentifier,
buildNumber: expoConfig.ios?.buildNumber,
};
const androidConfig = {
androidPackage: expoConfig.android?.package,
versionCode: expoConfig.android?.versionCode,
};
if (
!(
!!version &&
Object.values(iOSConfig).every((prop) => !!prop) &&
Object.values(androidConfig).every((prop) => !!prop)
)
) {
throw new Error("Failed to retrieve Expo config");
return;
}
const { bundleIdentifier, buildNumber } = iOSConfig;
const { androidPackage, versionCode } = androidConfig;
const sentryConfig = expoConfig.hooks?.postPublish?.find((h) =>
h.file?.includes("upload-sourcemaps")
)?.config;
const childProcessEnv = Object.assign({}, process.env, {
SENTRY_ORG:
sentryConfig?.organization || process.env.SENTRY_ORG,
SENTRY_PROJECT:
sentryConfig?.project || process.env.SENTRY_PROJECT,
SENTRY_AUTH_TOKEN: sentryConfig?.authToken || process.env.SENTRY_AUTH_TOKEN,
SENTRY_URL: "https://sentry.io/",
});
const uploadIOSSourcemaps = spawnAsync(
"node_modules/@sentry/cli/bin/sentry-cli",
[
"releases",
"files",
`${bundleIdentifier}@${version}+${buildNumber}`,
"upload-sourcemaps",
"--dist",
iosUpdateId,
"--bundle",
`dist/bundles/main.jsbundle`,
"--bundle-sourcemap",
`dist/bundles/${iosMap}`,
"--no-dedupe",
],
{ env: childProcessEnv }
);
const uploadAndroidSourcemaps = spawnAsync(
"node_modules/@sentry/cli/bin/sentry-cli",
[
"releases",
"files",
`${androidPackage}@${version}+${versionCode}`,
"upload-sourcemaps",
"--dist",
androidUpdateId,
"--bundle",
`dist/bundles/index.android.bundle`,
"--bundle-sourcemap",
`dist/bundles/${androidMap}`,
"--no-dedupe",
],
{ env: childProcessEnv }
);
uploadIOSSourcemaps.child.stdout?.on("data", (data) => {
console.log(
data
.toString("utf8")
.split("\n")
.map((l) => `[Upload iOS sourcemaps] ${l}`)
.join("\n")
);
});
uploadIOSSourcemaps.child.on("error", (error) => {
throw error;
});
uploadAndroidSourcemaps.child.stdout?.on("data", (data) => {
console.log(
data
.toString("utf8")
.split("\n")
.map((l) => `[Upload Android sourcemaps] ${l}`)
.join("\n")
);
});
uploadAndroidSourcemaps.child.on("error", (error) => {
throw error;
});
await uploadIOSSourcemaps;
await uploadAndroidSourcemaps;
};
async function uploadSourcemaps() {
const output = fs.readFileSync("output.txt", "utf8").split("\n");
const iosUpdateId = findUpdateId(output, "ios");
const androidUpdateId = findUpdateId(output, "android");
const bundles = getBundles();
if (!Object.values(bundles).every((b) => !!b)) {
console.log("Bundles could not be found\n");
return;
}
const { iosBundle, iosMap, androidBundle, androidMap } = bundles;
fs.renameSync(`dist/bundles/${iosBundle}`, "dist/bundles/main.jsbundle");
fs.renameSync(
`dist/bundles/${androidBundle}`,
"dist/bundles/index.android.bundle"
);
try {
await uploadSourcemapsAsync({
iosUpdateId: iosUpdateId,
iosMap: iosMap,
androidUpdateId: androidUpdateId,
androidMap: androidMap,
});
} catch (error) {
console.log(error.message + "\n");
return;
}
}
uploadSourcemaps();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment