diff --git a/.github/actions/check_clean/action.yml b/.github/actions/check_clean/action.yml
new file mode 100644
index 0000000000..484958480f
--- /dev/null
+++ b/.github/actions/check_clean/action.yml
@@ -0,0 +1,12 @@
+name: check_clean
+runs:
+  using: composite
+  steps:
+  - name: Check Repository Clean
+    run: |-
+      # Print status for debugging on CircleCI.
+      git status
+      # Fail the build if any step leaves uncommitted changes in the repo
+      # that would prevent Lerna from publishing (Lerna gets this right).
+      git diff --exit-code
+    shell: bash
diff --git a/.github/actions/count_deps/action.yml b/.github/actions/count_deps/action.yml
new file mode 100644
index 0000000000..f5c84584c2
--- /dev/null
+++ b/.github/actions/count_deps/action.yml
@@ -0,0 +1,18 @@
+name: count_deps
+runs:
+  using: composite
+  steps:
+  - name: Count Generated Project Dependencies
+    # TODO: Can TOTAL_PACKAGES be exported in a cleaner way?
+    run: |-
+      MAX_PACKAGES="2260"
+      total=$(./scripts/count-dependencies.js generated-${{ matrix.template }})
+      echo "TOTAL_PACKAGES=${total}" >> $GITHUB_ENV
+
+      if [ "$total" -gt "$MAX_PACKAGES" ]; then
+          echo "Error: Found $TOTAL_PACKAGES installed packages (max $MAX_PACKAGES).";
+          exit 1;
+      else
+          echo "Found $TOTAL_PACKAGES installed packages (max $MAX_PACKAGES).";
+      fi
+    shell: bash
diff --git a/.github/actions/create_mrt/action.yml b/.github/actions/create_mrt/action.yml
new file mode 100644
index 0000000000..9c9f153884
--- /dev/null
+++ b/.github/actions/create_mrt/action.yml
@@ -0,0 +1,14 @@
+name: create_mrt
+inputs:
+  mobify_user:
+    description: "Mobify user email"
+  mobify_api_key:
+    description: "Mobify user API key"
+runs:
+  using: composite
+  steps:
+    - name: Create MRT credentials file
+      run: |-
+        # Add credentials file at ~/.mobify so we can upload to Mobify Cloud
+        npm run save-credentials --prefix packages/template-retail-react-app -- --user "${{inputs.mobify_user}}" --key "${{inputs.mobify_api_key}}"
+      shell: bash
diff --git a/.github/actions/datadog/action.yml b/.github/actions/datadog/action.yml
new file mode 100644
index 0000000000..a35bcc51fc
--- /dev/null
+++ b/.github/actions/datadog/action.yml
@@ -0,0 +1,16 @@
+name: datadog
+inputs:
+  datadog_api_key:
+    description: "Datadog API key"
+  TOTAL_PACKAGES:
+    description: "Total # of packages"
+runs:
+  using: composite
+  steps:
+    - name: Send metrics to Datadog
+      run : |
+        # Add a dogrc so we can submit metrics to datadog
+        printf "[Connection]\napikey = ${{inputs.datadog_api_key}}\nappkey =\n" > ~/.dogrc
+
+        dog metric post mobify_platform_sdks.generated_project_total_packages ${{ inputs.TOTAL_PACKAGES }}
+      shell: bash
diff --git a/.github/actions/lighthouse_ci/action.yml b/.github/actions/lighthouse_ci/action.yml
new file mode 100644
index 0000000000..3b6474959a
--- /dev/null
+++ b/.github/actions/lighthouse_ci/action.yml
@@ -0,0 +1,7 @@
+name: lighthouse_ci
+runs:
+  using: composite
+  steps:
+  - name: Run Lighthouse CI on the PWA
+    run: npm run test:lighthouse --prefix packages/template-retail-react-app
+    shell: bash
diff --git a/.github/actions/publish_to_npm/action.yml b/.github/actions/publish_to_npm/action.yml
new file mode 100644
index 0000000000..0f9267ea5c
--- /dev/null
+++ b/.github/actions/publish_to_npm/action.yml
@@ -0,0 +1,21 @@
+name: publish_to_npm
+inputs:
+  NODE_AUTH_TOKEN:
+    description: "Node auth token"
+runs:
+  using: composite
+  steps:
+  # TODO: Figure out a way to specify whether to publish to "latest" or "next" tag
+  - name: Publish to NPM
+    run: |-
+      # Add NPM token to allow publishing
+      echo "//registry.npmjs.org/:_authToken=${{ inputs.NODE_AUTH_TOKEN }}" > ~/.npmrc
+
+      # Publish all changed packages. The "from-package" arg means "look
+      # at the version numbers in each package.json file and if that doesn't
+      # exist on NPM, publish"
+      npm run lerna -- publish from-package --yes --no-verify-access
+
+      # Cleanup
+      rm ~/.npmrc
+    shell: bash
diff --git a/.github/actions/push_to_mrt/action.yml b/.github/actions/push_to_mrt/action.yml
new file mode 100644
index 0000000000..c7affda607
--- /dev/null
+++ b/.github/actions/push_to_mrt/action.yml
@@ -0,0 +1,18 @@
+name: push_to_mrt
+inputs:
+  CWD:
+    description: Project directory
+  TARGET:
+    description: MRT target
+runs:
+  using: composite
+  steps:
+    - name: Push Bundle to MRT
+      run: |-
+        cd ${{ inputs.CWD }}
+        project="scaffold-pwa"
+        build="build ${{ github.run_id }} on ${{ github.ref }} (${{ github.sha }})"
+        if [[ ${{ inputs.TARGET }} ]]; then
+          npm run push -- -s $project --message "$build" --target ${{ inputs.TARGET }}
+        fi
+      shell: bash
diff --git a/.github/actions/setup_ubuntu/action.yml b/.github/actions/setup_ubuntu/action.yml
new file mode 100644
index 0000000000..87b24e699e
--- /dev/null
+++ b/.github/actions/setup_ubuntu/action.yml
@@ -0,0 +1,35 @@
+name: setup_ubuntu
+inputs:
+  cwd:
+    required: false
+    default: "${PWD}"
+description: "Setup Ubuntu Machine"
+runs:
+  using: composite
+  steps:
+    - name: Install Dependencies
+      run: |-
+        # Install system dependencies
+        sudo apt-get update -yq
+        sudo apt-get install python2 python3-pip time -yq
+        sudo pip install -U pip setuptools
+        sudo pip install awscli==1.18.85 datadog==0.40.1
+
+        # Install node dependencies
+        node ./scripts/gtime.js monorepo_install npm ci
+
+        # Build the PWA
+        npm run lerna -- run analyze-build --scope "retail-react-app"
+
+        # Report bundle sizes
+        node ./scripts/report-bundle-size.js
+
+        # Check that packages are all using the same versions of compilers, etc.
+        node ./scripts/check-dependencies.js
+
+        # Install Snyk CLI
+        sudo npm install -g snyk
+
+        # Install Lighthouse CI CLI
+        sudo npm install -g @lhci/cli
+      shell: bash
diff --git a/.github/actions/setup_windows/action.yml b/.github/actions/setup_windows/action.yml
new file mode 100644
index 0000000000..bea04deb09
--- /dev/null
+++ b/.github/actions/setup_windows/action.yml
@@ -0,0 +1,14 @@
+name: setup_windows
+inputs:
+  cwd:
+    required: false
+    default: "${PWD}"
+description: "Setup Windows Machine"
+runs:
+  using: composite
+  steps:
+    - name: Install Dependencies
+      run: |-
+        # Install node dependencies
+        npm ci
+      shell: bash
diff --git a/.github/actions/smoke_tests/action.yml b/.github/actions/smoke_tests/action.yml
new file mode 100644
index 0000000000..135e6aca25
--- /dev/null
+++ b/.github/actions/smoke_tests/action.yml
@@ -0,0 +1,14 @@
+name: smoke_tests
+inputs:
+  dir:
+    required: false
+    # The path to a project to test
+    default: "./packages/template-retail-react-app"
+runs:
+  using: composite
+  steps:
+  - name: Smoke test scripts
+    run: |-
+      # Basic smoke-tests for uncommonly run scripts in a project
+      node ./scripts/smoke-test-npm-scripts.js --dir ${{ inputs.dir }}
+    shell: bash
diff --git a/.github/actions/snyk/action.yml b/.github/actions/snyk/action.yml
new file mode 100644
index 0000000000..fb3cbe0541
--- /dev/null
+++ b/.github/actions/snyk/action.yml
@@ -0,0 +1,15 @@
+name: snyk
+inputs:
+  snyk_token:
+    description: "Snyk token"
+  DEVELOP:
+    description: "Is this the 'develop' branch?"
+runs:
+  using: composite
+  steps:
+  - name: Audit Generated Project
+    run: |-
+        # Run snyk auth - authenticate snyk using environment variables to add the token
+        snyk auth ${{ inputs.snyk_token }}
+        snyk monitor --ignore-policy --remote-repo-url='https://github.com/SalesforceCommerceCloud/pwa-kit.git' --project-name='generated-scaffold-pwa'
+    shell: bash
diff --git a/.github/actions/unit_tests/action.yml b/.github/actions/unit_tests/action.yml
new file mode 100644
index 0000000000..2b7bbe010c
--- /dev/null
+++ b/.github/actions/unit_tests/action.yml
@@ -0,0 +1,27 @@
+name: unit_tests
+inputs:
+  cwd:
+    required: false
+    default: "${PWD}"
+description: "Run tests action description"
+runs:
+  using: composite
+  steps:
+  - name: Run tests step
+    # TODO: The pilefile policy is a legacy of CircleCI. Is it still needed?
+    run: |-
+      # Explicitly set pipefile policy. This is the default for non-windows, but seems
+      # that is needs to be set on windows to fail immediately.
+      set -eo pipefail
+
+      cd ${{ inputs.cwd }}
+
+      # Note: Each of these test commands need to be exposed on the monorepo
+      # root and *also* on the PWA package. This section is run on both.
+
+      # Ensure bundlesize is in check
+      npm run test:max-file-size
+
+      # Always run fast unit tests
+      npm run test
+    shell: bash
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000000..d11228af86
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,277 @@
+# WARNING! Conditionals are set as variables to minimize repetitive checks.
+# However, this results in the variables being the *string* values "true" or "false".
+# As a result, you must always explicitly check for those strings. For example,
+# ${{ env.DEVELOP }} will ALWAYS evaluate as true; to achieve the expected result
+# you must check ${{ env.DEVELOP == 'true' }}. There's probably a better way to DRY,
+# but this is what we have for now.
+
+name: SalesforceCommerceCloud/pwa-kit/test
+on:
+  # PRs from forks trigger `pull_request`, but do NOT have access to secrets.
+  # More info:
+  # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories-1
+  # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+  # https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks
+  pull_request: # Default: opened, reopened, synchronize (head branch updated)
+  push:
+    branches:
+      - develop
+      # TODO: Should we run on all pushes to release branches, or should we run on GitHub releases?
+      - 'release-*'
+  schedule:
+    - cron: 0 8 * * *
+env:
+  IS_NOT_FORK: ${{ github.repository == 'SalesforceCommerceCloud/pwa-kit' }}
+  DEVELOP: ${{ github.repository == 'SalesforceCommerceCloud/pwa-kit' && github.ref == 'refs/heads/develop' }}
+  RELEASE: ${{ github.repository == 'SalesforceCommerceCloud/pwa-kit' && startsWith(github.ref, 'refs/heads/release-') }}
+jobs:
+  pwa-kit:
+    strategy:
+      matrix:
+        node: [14]
+        npm: [6, 7, 8]
+    runs-on: ubuntu-latest
+    env:
+      # The "default" npm is the one that ships with a given version of node
+      # node v14 uses npm@6, latest node v16 uses npm@8
+      # For more: https://nodejs.org/en/download/releases/
+      IS_DEFAULT_NPM: ${{ matrix.npm == 6 }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node }}
+          cache: npm
+
+      - name: Update NPM version
+        if: env.IS_DEFAULT_NPM == 'false'
+        run: |-
+          npm install -g npm@${{ matrix.npm }}
+
+      - name: Setup Ubuntu Machine
+        uses: "./.github/actions/setup_ubuntu"
+
+      - name: Run unit tests
+        uses: "./.github/actions/unit_tests"
+
+      - name: Run Lighthouse CI on the PWA
+        if: env.IS_DEFAULT_NPM == 'true'
+        uses: "./.github/actions/lighthouse_ci"
+
+      - name: Smoke test scripts
+        if: env.IS_DEFAULT_NPM == 'true'
+        uses: "./.github/actions/smoke_tests"
+
+      - name: Create MRT credentials file
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true'
+        uses: "./.github/actions/create_mrt"
+        with:
+          mobify_user: ${{ secrets.MOBIFY_CLIENT_USER }}
+          mobify_api_key: ${{ secrets.MOBIFY_CLIENT_API_KEY }}
+
+      - name: Push Bundle to MRT (Development)
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.DEVELOP == 'true'
+        uses: "./.github/actions/push_to_mrt"
+        with:
+          CWD: "./packages/template-retail-react-app"
+          TARGET: staging
+
+      - name: Push Bundle to MRT (Production)
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.RELEASE == 'true'
+        uses: "./.github/actions/push_to_mrt"
+        with:
+          CWD: "./packages/template-retail-react-app"
+          TARGET: production
+
+      - name: Push Bundle to MRT (Commerce SDK React)
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.DEVELOPMENT == 'true'
+        uses: "./.github/actions/push_to_mrt"
+        with:
+          CWD: "./packages/test-commerce-sdk-react"
+          TARGET: commerce-sdk-react
+
+      - name: Check Repository Clean
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true'
+        uses: "./.github/actions/check_clean"
+
+      - name: Publish to NPM
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.RELEASE == 'true'
+        uses: "./.github/actions/publish_to_npm"
+        with:
+          NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
+
+      - name: Send GitHub Action data to Slack workflow (PWA Kit)
+        id: slack
+        if: env.IS_NOT_FORK == 'true' && env.IS_DEFAULT_NPM == 'true' && env.DEVELOP == 'true' && failure()
+        uses: slackapi/slack-github-action@v1.23.0
+        with:
+          payload: |
+            {
+              "test": "testNode${{ matrix.node }}"
+            }
+        env:
+          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+  pwa-kit-windows:
+    strategy:
+        # TODO: We don't *need* a matrix with single values,
+        # but is it worth keeping for supporting multiple versions in the future?
+        matrix:
+          node: [14]
+          npm: [6]
+    runs-on: windows-latest
+    steps:
+        - name: Checkout
+          uses: actions/checkout@v3
+
+        - name: Setup Node
+          uses: actions/setup-node@v3
+          with:
+            node-version: ${{ matrix.node }}
+            cache: npm
+
+        - name: Setup Windows Machine
+          uses: "./.github/actions/setup_windows"
+
+        - name: Run tests
+          uses: "./.github/actions/unit_tests"
+
+  # TODO: The generated workflow is identical to the generated-windows workflow,
+  # with a few extra steps. Can the workflows be merged? (Add `os` to the matrix?)
+  generated:
+    strategy:
+      matrix:
+        template: [test-project, retail-react-app-demo, express-minimal-test-project, typescript-minimal-test-project]
+    runs-on: ubuntu-latest
+    env:
+      IS_TEMPLATE_FROM_RETAIL_REACT_APP: ${{ matrix.template == 'test-project' || matrix.template == 'retail-react-app-demo' }}
+      PROJECT_DIR: generated-${{ matrix.template }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 14
+
+      - name: Setup Ubuntu Machine
+        uses: "./.github/actions/setup_ubuntu"
+
+      - name: Generate ${{ matrix.template }} project
+        run: |-
+          node packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js --outputDir ${{ env.PROJECT_DIR }}
+        env:
+          GENERATOR_PRESET: ${{ matrix.template }}
+        timeout-minutes: 5
+
+      - name: Run unit tests
+        if: env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/unit_tests"
+        with:
+          cwd: ${{ env.PROJECT_DIR }}
+
+      - name: Run smoke tests
+        if: env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/smoke_tests"
+        with:
+          dir: ${{ env.PROJECT_DIR }}
+
+      - name: Count Generated Project Dependencies
+        id: count_deps
+        if: env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/count_deps"
+
+      - name: Store Verdaccio logfile artifact
+        uses: actions/upload-artifact@v3
+        with:
+          path: packages/pwa-kit-create-app/local-npm-repo/verdaccio.log
+
+      - name: Audit Generated Project
+        if: env.IS_NOT_FORK == 'true' && env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true' && env.DEVELOP == 'true'
+        uses: "./.github/actions/snyk"
+        with:
+          snyk_token: ${{ secrets.SNYK_TOKEN }}
+
+      - name: Send metrics to Datadog
+        if: env.IS_NOT_FORK == 'true' && env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/datadog"
+        with:
+          datadog_api_key: ${{ secrets.DATADOG_API_KEY }}
+          # TODO: The way this is set is a little bit magic - can it be cleaned up?
+          TOTAL_PACKAGES: $TOTAL_PACKAGES
+
+      - name: Create MRT credentials file
+        if: env.IS_NOT_FORK == 'true' && env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/create_mrt"
+        with:
+          mobify_user: ${{ secrets.MOBIFY_CLIENT_USER }}
+          mobify_api_key: ${{ secrets.MOBIFY_CLIENT_API_KEY }}
+
+      - name: Push Bundle to MRT
+        if: env.IS_NOT_FORK == 'true' && env.DEVELOP == 'true' && matrix.template == 'test-project'
+        uses: "./.github/actions/push_to_mrt"
+        with:
+          CWD: ${{ env.PROJECT_DIR }}
+          TARGET: generated-pwa
+
+      - name: Send GitHub Action data to Slack workflow (Generated)
+        id: slack
+        if: env.IS_NOT_FORK == 'true' && env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true' && env.DEVELOP == 'true' && failure()
+        uses: slackapi/slack-github-action@v1.23.0
+        with:
+          payload: |
+            {
+              "test": "generated ${{ matrix.template }}"
+            }
+        env:
+          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+  generated-windows:
+    strategy:
+      matrix:
+        template: [test-project, retail-react-app-demo, express-minimal-test-project, typescript-minimal-test-project]
+    runs-on: windows-latest
+    env:
+      IS_TEMPLATE_FROM_RETAIL_REACT_APP: ${{ matrix.template == 'test-project' || matrix.template == 'retail-react-app-demo' }}
+      PROJECT_DIR: generated-${{ matrix.template }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 14
+
+      - name: Setup Windows Machine
+        uses: "./.github/actions/setup_windows"
+
+      - name: Generate ${{ matrix.template }} project
+        run: |-
+          node packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js --outputDir generated-${{ matrix.template }}
+        env:
+          GENERATOR_PRESET: ${{ matrix.template }}
+        timeout-minutes: 5
+
+      - name: Run unit tests
+        if: env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/unit_tests"
+        with:
+          cwd: ${{ env.PROJECT_DIR }}
+
+      - name: Run smoke tests
+        if: env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/smoke_tests"
+        with:
+          dir: ${{ env.PROJECT_DIR }}
+
+      - name: Count Generated Project Dependencies
+        if: env.IS_TEMPLATE_FROM_RETAIL_REACT_APP == 'true'
+        uses: "./.github/actions/count_deps"
+
+      - name: Store Verdaccio logfile artifact
+        uses: actions/upload-artifact@v3
+        with:
+          path: packages/pwa-kit-create-app/local-npm-repo/verdaccio.log