diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000000..77ce8bc0da
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,81 @@
+name: Publish a New Release
+on:
+  pull_request:
+    branches: [main]
+    types: [closed]
+    
+jobs:
+  publish_to_npm:
+    name: Publish to NPM and GitHub
+    runs-on: ubuntu-latest
+    steps:
+
+      # Determine if PR was a valid merged release PR. If so, publish the new release. Otherwise, do nothing.
+      # This is a valid release PR if the following 3 things are true:
+      #   1. The head (source) branch of the PR matches the expected "release/x.y.z" pattern
+      #   2. The PR has been merged, rather than closed
+      #   3. The head (source) repo of the PR is not a fork, or in other words the PR is made from a branch within the main repo
+      - name: Validate Release PR
+        id: validate_pr
+        run: |
+          merged=${{ github.event.pull_request.merged }}
+          forked=${{ github.event.pull_request.head.repo.fork }}
+          echo "Source branch is ${{ github.head_ref }}; merged = $merged; from a fork = $forked"
+          if [ $(grep -E '^release/[0-9]+\.[0-9]+\..+$' <<< '${{ github.head_ref }}') ] && [ $merged = true ] && [ $forked = false ]
+          then
+            echo "::set-output name=is_release::true"
+          fi
+
+      # Get release version and draft release tag from PR metadata
+      - name: Get Release Metadata
+        if: steps.validate_pr.outputs.is_release
+        id: metadata
+        run: |
+          version=$(cut -d'/' -f2 <<< '${{ github.head_ref }}')
+          echo "::set-output name=version::$version"
+
+      - name: Checkout Repository
+        if: steps.validate_pr.outputs.is_release
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+
+      - name: Setup Node
+        uses: actions/setup-node@v2
+        with:
+          node-version: '14.x'
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Publish to NPM
+        if: steps.validate_pr.outputs.is_release
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+        run: ./scripts/publish.sh
+
+      # Generates changelog without the "Unreleased" portion for use in release body
+      # Newlines must be URL-encoded to render properly in the GitHub UI
+      - name: Generate Release Body
+        if: steps.validate_pr.outputs.is_release
+        id: generate-changelog
+        env:
+          GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          description=$(npx lerna-changelog | sed '1,3d')
+          description="${description//'%'/'%25'}"
+          description="${description//$'\n'/'%0A'}"
+          description="${description//$'\r'/'%0D'}"
+          echo "::set-output name=CHANGELOG::$description"
+
+      - name: Create GitHub Release
+        if: steps.validate_pr.outputs.is_release
+        id: make-release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: 'v${{ steps.metadata.outputs.version }}'
+          commitish: main
+          release_name: 'v${{ steps.metadata.outputs.version }} Release'
+          body: "${{ steps.generate-changelog.outputs.CHANGELOG }}"
+          draft: false
+          prerelease: false
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
new file mode 100644
index 0000000000..3e29aa3382
--- /dev/null
+++ b/.github/workflows/release-pr.yml
@@ -0,0 +1,63 @@
+name: Create New Release PR
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: The semver-compliant version to tag the release with, e.g. 1.2.3, 1.0.0-rc.1
+        required: true
+
+jobs:
+  create_release_pr:
+    name: Create Release PR and Draft Release
+    runs-on: ubuntu-latest
+    steps: 
+      - name: Checkout Repository
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+
+      - name: Setup Node
+        uses: actions/setup-node@v2
+        with:
+          node-version: '14.x'
+
+      - name: Cache Dependencies
+        uses: actions/cache@v2
+        with:
+          path: |
+            node_modules
+            package-lock.json
+            detectors/node/*/node_modules
+            metapackages/*/node_modules
+            packages/*/node_modules
+            plugins/node/*/node_modules
+            plugins/web/*/node_modules
+            propagators/*/node_modules
+          key: ${{ runner.os }}-${{ matrix.container }}-${{ hashFiles('**/package.json') }}
+
+      # Bump versions in all package.json and version.ts files
+      - name: Prepare Release
+        run: |
+          npm install
+          npm --no-git-tag-version version ${{ github.event.inputs.version }}
+          npx lerna publish ${{ github.event.inputs.version }} --skip-npm --no-git-tag-version --no-push --yes
+          npx lerna bootstrap --no-ci
+
+      - name: Update Changelog
+        env:
+          GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
+        run: ./scripts/changelog-update.sh ${{ github.event.inputs.version }}
+
+      # Make PR with version bumps and changelog update. Merging this PR triggers publish workflow.
+      # See: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/.github/workflows/publish.yml
+      - name: Create Release PR
+        uses: peter-evans/create-pull-request@v3
+        with:
+          branch: release/${{ github.event.inputs.version }}
+          commit-message: 'chore: ${{ github.event.inputs.version }} release proposal'
+          title: 'chore: ${{ github.event.inputs.version }} release proposal'
+          body: | 
+            This is an auto-generated release PR. If additional changes need to be incorporated before the release, the CHANGELOG must be updated manually.
+            
+            Merging this PR will automatically release all packages to NPM.
+          delete-branch: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9a4bbf588..e8cd5e07e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 # CHANGELOG
 
-All notable changes to this project will be documented in this file.
+All notable changes to this project will be documented in this file. Do not remove the "Unreleased" header; it is used in the automated release workflow.
 
 ## Unreleased
 
diff --git a/RELEASING.md b/RELEASING.md
index e124d0c6fc..cdbac1c1b7 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -2,7 +2,31 @@
 
 This document explains how to publish all OT modules at version x.y.z. Ensure that you’re following semver when choosing a version number.
 
-Release Process:
+## Auto-generate a Release PR
+
+Navigate to the [Create a Release PR Worflow](https://github.com/open-telemetry/opentelemetry-js-contrib/actions/workflows/release-pr.yml). Click the "Run workflow" dropdown, enter the semver-compliant version to release, then click "Run workflow". It will generate a release pull request from a branch named like `release/x.y.z`.
+
+## Review Release PR
+
+If necessary, update sample code or documentation on the `release/x.y.z` branch to be included in the release. Another maintainer should approve the PR, specifically validating the automatic updates and verifying the tests are passing.
+
+NOTE: If any major changes are merged while the release PR is open, the CHANGELOG on the `release/x.y.z` branch must be manually updated to reflect the change.
+
+## Merge Release PR
+
+After approval, merge the release PR. This will automatically publish all packages. Verify:
+
+* The [publish workflow](https://github.com/open-telemetry/opentelemetry-js-contrib/actions/workflows/publish.yml) runs successfully
+* The new version is available in NPM, e.g. for the [Express instrumentation package](https://www.npmjs.com/package/@opentelemetry/instrumentation-express)
+* A [GitHub Release](https://github.com/open-telemetry/opentelemetry-js-contrib/releases) was cut correctly, with a tag pointing to the new version, and a body of the changelog
+
+That's it! No need to read on unless something above went wrong.
+
+# Manual Publishing Process
+
+If any step of the above automated process fails, complete the release manually by picking up at the failed step.
+
+Manual Release Process Steps:
 
 * [Update to latest locally](#update-to-latest-locally)
 * [Create a new branch](#create-a-new-branch)
diff --git a/lerna.json b/lerna.json
index f0761e6bc4..f367998057 100644
--- a/lerna.json
+++ b/lerna.json
@@ -21,6 +21,7 @@
       "feature-request": ":sparkles: (Feature)",
       "internal": ":house: Internal"
     },
-    "cacheDir": ".changelog"
+    "cacheDir": ".changelog",
+    "ignoreCommitters": ["github-actions"]
   }
 }
diff --git a/package.json b/package.json
index 42b83f0d8d..dc3c578224 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
     "compile": "lerna run compile",
     "test": "lerna run test",
     "test:browser": "lerna run test:browser",
-    "bootstrap": "lerna bootstrap",
+    "bootstrap": "lerna bootstrap --no-ci",
     "bump": "lerna publish",
     "codecov": "lerna run codecov",
     "codecov:browser": "lerna run codecov:browser",
diff --git a/scripts/changelog-update.sh b/scripts/changelog-update.sh
new file mode 100755
index 0000000000..2ac2c2e1f1
--- /dev/null
+++ b/scripts/changelog-update.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+echo "
+## $1
+" > delete_me.txt
+npx lerna-changelog | sed '1,3d' >> delete_me.txt
+sed -i -e '/## Unreleased/r delete_me.txt' CHANGELOG.md
+rm delete_me.txt
diff --git a/scripts/publish.sh b/scripts/publish.sh
new file mode 100755
index 0000000000..a1a6a81a91
--- /dev/null
+++ b/scripts/publish.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+for path in $(cat lerna.json | jq '.packages[]'); do
+  base=$(sed 's/"//g' <<< $path)  # Remove quotes
+  for package in $base; do
+    if [ -d $package ]; then
+      echo Publishing to NPM: $package
+      pushd $package
+      npm publish
+      popd
+    fi
+  done
+done