name: (Runtime) Commit Artifacts for Meta WWW and fbsource V2

on:
  workflow_run:
    workflows: ["(Runtime) Build and Test"]
    types: [completed]
    branches:
      - main
  workflow_dispatch:
    inputs:
      commit_sha:
        required: false
        type: string
      force:
        description: 'Force a commit to the builds/... branches'
        required: true
        default: false
        type: boolean
      dry_run:
        description: Perform a dry run (run everything except push)
        required: true
        default: false
        type: boolean

permissions: {}

env:
  TZ: /usr/share/zoneinfo/America/Los_Angeles
  # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
  SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1

jobs:
  download_artifacts:
    runs-on: ubuntu-latest
    permissions:
      # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
      actions: read
    steps:
      - uses: actions/checkout@v4
      - name: Restore cached node_modules
        uses: actions/cache@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd scripts/release install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Download artifacts for base revision
        run: |
          GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
      - name: Display structure of build
        run: ls -R build
      - name: Archive build
        uses: actions/upload-artifact@v4
        with:
          name: build
          path: build/
          if-no-files-found: error


  process_artifacts:
    runs-on: ubuntu-latest
    needs: [download_artifacts]
    outputs:
      www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
      fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
      last_version_classic: ${{ steps.get_last_version_www.outputs.last_version_classic }}
      last_version_modern: ${{ steps.get_last_version_www.outputs.last_version_modern }}
      last_version_rn: ${{ steps.get_last_version_rn.outputs.last_version_rn }}
      current_version_classic: ${{ steps.get_current_version.outputs.current_version_classic }}
      current_version_modern: ${{ steps.get_current_version.outputs.current_version_modern }}
      current_version_rn: ${{ steps.get_current_version.outputs.current_version_rn }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: builds/facebook-www
      - name: "Get last version string for www"
        id: get_last_version_www
        run: |
          # Empty checks only needed for backwards compatibility,can remove later.
          VERSION_CLASSIC=$( [ -f ./compiled/facebook-www/VERSION_CLASSIC ] && cat ./compiled/facebook-www/VERSION_CLASSIC || echo '' )
          VERSION_MODERN=$( [ -f ./compiled/facebook-www/VERSION_MODERN ] && cat ./compiled/facebook-www/VERSION_MODERN || echo '' )
          echo "Last classic version is $VERSION_CLASSIC"
          echo "Last modern version is $VERSION_MODERN"
          echo "last_version_classic=$VERSION_CLASSIC" >> "$GITHUB_OUTPUT"
          echo "last_version_modern=$VERSION_MODERN" >> "$GITHUB_OUTPUT"
      - uses: actions/checkout@v4
        with:
          ref: builds/facebook-fbsource
      - name: "Get last version string for rn"
        id: get_last_version_rn
        run: |
          # Empty checks only needed for backwards compatibility,can remove later.
          VERSION_NATIVE_FB=$( [ -f ./compiled-rn/VERSION_NATIVE_FB ] && cat ./compiled-rn/VERSION_NATIVE_FB || echo '' )
          echo "Last rn version is $VERSION_NATIVE_FB"
          echo "last_version_rn=$VERSION_NATIVE_FB" >> "$GITHUB_OUTPUT"
      - uses: actions/checkout@v4
      - name: "Check branches"
        id: check_branches
        run: |
          echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
          echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
      - name: Restore downloaded build
        uses: actions/download-artifact@v4
        with:
          name: build
          path: build
      - name: Display structure of build
        run: ls -R build
      - name: Strip @license from eslint plugin and react-refresh
        run: |
          sed -i -e 's/ @license React*//' \
            build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
            build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
      - name: Insert @headers into eslint plugin and react-refresh
        run: |
          sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
            build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
            build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
      - name: Move relevant files for React in www into compiled
        run: |
          # Move the facebook-www folder into compiled
          mkdir ./compiled
          mv build/facebook-www ./compiled

          # Move ReactAllWarnings.js to facebook-www
          mkdir ./compiled/facebook-www/__test_utils__
          mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js

          # Copy eslint-plugin-react-hooks
          mkdir ./compiled/eslint-plugin-react-hooks
          cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
            ./compiled/eslint-plugin-react-hooks/index.js

          # Move unstable_server-external-runtime.js into facebook-www
          mv build/oss-experimental/react-dom/unstable_server-external-runtime.js \
            ./compiled/facebook-www/unstable_server-external-runtime.js

          # Move react-refresh-babel.development.js into babel-plugin-react-refresh
          mkdir ./compiled/babel-plugin-react-refresh
          mv build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js \
            ./compiled/babel-plugin-react-refresh/index.js

          ls -R ./compiled
      - name: Move relevant files for React in fbsource into compiled-rn
        run: |
          BASE_FOLDER='compiled-rn/facebook-fbsource/xplat/js'
          mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/
          mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-dom,react-is,react-test-renderer}/

          # Move React Native renderer
          mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
          mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
          mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/
          mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/
          mv build/facebook-react-native/react-dom/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/
          mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
          mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/

          # Delete the OSS renderers, these are sync'd to RN separately.
          RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
          rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
          
          # Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
          SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
          rm $SHIM_FOLDER/ReactNative.js

          # Copy eslint-plugin-react-hooks
          # NOTE: This is different from www, here we include the full package
          #       including package.json to include dependencies in fbsource.
          mkdir "$BASE_FOLDER/tools"
          cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"

          # Move React Native version file
          mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB

          ls -R ./compiled-rn
      - name: Add REVISION files
        run: |
          echo ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} >> ./compiled/facebook-www/REVISION
          cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
          echo ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
      - name: "Get current version string"
        id: get_current_version
        run: |
          VERSION_CLASSIC=$(cat ./compiled/facebook-www/VERSION_CLASSIC)
          VERSION_MODERN=$(cat ./compiled/facebook-www/VERSION_MODERN)
          VERSION_NATIVE_FB=$(cat ./compiled-rn/VERSION_NATIVE_FB)
          echo "Current classic version is $VERSION_CLASSIC"
          echo "Current modern version is $VERSION_MODERN"
          echo "Current rn version is $VERSION_NATIVE_FB"
          echo "current_version_classic=$VERSION_CLASSIC" >> "$GITHUB_OUTPUT"
          echo "current_version_modern=$VERSION_MODERN" >> "$GITHUB_OUTPUT"
          echo "current_version_rn=$VERSION_NATIVE_FB" >> "$GITHUB_OUTPUT"
      - uses: actions/upload-artifact@v4
        with:
          name: compiled
          path: compiled/
          if-no-files-found: error
      - uses: actions/upload-artifact@v4
        with:
          name: compiled-rn
          path: compiled-rn/
          if-no-files-found: error

  commit_www_artifacts:
    needs: [download_artifacts, process_artifacts]
    if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.process_artifacts.outputs.www_branch_count == '0')
    runs-on: ubuntu-latest
    permissions:
      # Used to push a commit to builds/facebook-www
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          ref: builds/facebook-www
      - name: Ensure clean directory
        run: rm -rf compiled
      - uses: actions/download-artifact@v4
        with:
          name: compiled
          path: compiled/
      - name: Revert version changes
        if: needs.process_artifacts.outputs.last_version_classic != '' && needs.process_artifacts.outputs.last_version_modern != ''
        env:
          CURRENT_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.current_version_classic }}
          CURRENT_VERSION_MODERN: ${{ needs.process_artifacts.outputs.current_version_modern }}
          LAST_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.last_version_classic }}
          LAST_VERSION_MODERN: ${{ needs.process_artifacts.outputs.last_version_modern }}
        run: |
          echo "Reverting $CURRENT_VERSION_CLASSIC to $LAST_VERSION_CLASSIC"
          grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled || echo "No files found with $CURRENT_VERSION_CLASSIC"
          grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_CLASSIC/$LAST_VERSION_CLASSIC/g"
          grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled || echo "Classic version reverted"
          echo "===================="
          echo "Reverting $CURRENT_VERSION_MODERN to $LAST_VERSION_MODERN"
          grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "No files found with $CURRENT_VERSION_MODERN"
          grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g"
          grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted"
      - name: Check for changes
        if: inputs.force != true
        id: check_should_commit
        run: |
          echo "Full git status"
          git add .
          git status
          echo "===================="
          if git status --porcelain | grep -qv '/REVISION'; then
            echo "Changes detected"
            echo "===== Changes ====="
            git --no-pager diff -U0 | grep '^[+-]' | head -n 50
            echo "==================="
            echo "should_commit=true" >> "$GITHUB_OUTPUT"
          else
            echo "No Changes detected"
            echo "should_commit=false" >> "$GITHUB_OUTPUT"
          fi
      - name: Re-apply version changes
        if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.process_artifacts.outputs.last_version_classic != '' && needs.process_artifacts.outputs.last_version_modern != '')
        env:
          CURRENT_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.current_version_classic }}
          CURRENT_VERSION_MODERN: ${{ needs.process_artifacts.outputs.current_version_modern }}
          LAST_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.last_version_classic }}
          LAST_VERSION_MODERN: ${{ needs.process_artifacts.outputs.last_version_modern }}
        run: |
          echo "Re-applying $LAST_VERSION_CLASSIC to $CURRENT_VERSION_CLASSIC"
          grep -rl "$LAST_VERSION_CLASSIC" ./compiled || echo "No files found with $LAST_VERSION_CLASSIC"
          grep -rl "$LAST_VERSION_CLASSIC" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_CLASSIC/$CURRENT_VERSION_CLASSIC/g"
          grep -rl "$LAST_VERSION_CLASSIC" ./compiled || echo "Classic version re-applied"
          echo "===================="
          echo "Re-applying $LAST_VERSION_MODERN to $CURRENT_VERSION_MODERN"
          grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "No files found with $LAST_VERSION_MODERN"
          grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g"
          grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied"
      - name: Will commit these changes
        if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
        run: |
          git add .
          git status
      - name: Check commit message
        if: inputs.dry_run
        run: |
          git fetch origin --quiet
          git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:"%B"
      - name: Commit changes to branch
        if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
        run: |
          git config --global user.email "${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}"
          git config --global user.name "${{ github.triggering_actor }}"

          git fetch origin --quiet
          git commit -m "$(git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:'%B%n%nDiffTrain build for [${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha}})')" || echo "No changes to commit"
      - name: Push changes to branch
        if: inputs.dry_run == false && (inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true')
        run: git push

  commit_fbsource_artifacts:
    needs: [download_artifacts, process_artifacts]
    permissions:
      # Used to push a commit to builds/facebook-fbsource
      contents: write
    if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.process_artifacts.outputs.fbsource_branch_count == '0')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: builds/facebook-fbsource
      - name: Ensure clean directory
        run: rm -rf compiled-rn
      - uses: actions/download-artifact@v4
        with:
          name: compiled-rn
          path: compiled-rn/
      - name: Revert version changes
        if: needs.process_artifacts.outputs.last_version_rn != ''
        env:
          CURRENT_VERSION: ${{ needs.process_artifacts.outputs.current_version_rn }}
          LAST_VERSION: ${{ needs.process_artifacts.outputs.last_version_rn }}
        run: |
          echo "Reverting $CURRENT_VERSION to $LAST_VERSION"
          grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "No files found with $CURRENT_VERSION"
          grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g"
          grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted"
      - name: Check for changes
        if: inputs.force != 'true'
        id: check_should_commit
        run: |
          echo "Full git status"
          git add .
          git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
          echo "===================="
          # Ignore REVISION or lines removing @generated headers.
          if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
            echo "Changes detected"
            echo "===== Changes ====="
            git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
            echo "==================="
            echo "should_commit=true" >> "$GITHUB_OUTPUT"
          else
            echo "No Changes detected"
            echo "should_commit=false" >> "$GITHUB_OUTPUT"
          fi
      - name: Re-apply version changes
        if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.process_artifacts.outputs.last_version_rn != '')
        env:
          CURRENT_VERSION: ${{ needs.process_artifacts.outputs.current_version_rn }}
          LAST_VERSION: ${{ needs.process_artifacts.outputs.last_version_rn }}
        run: |
          echo "Re-applying $LAST_VERSION to $CURRENT_VERSION"
          grep -rl "$LAST_VERSION" ./compiled-rn || echo "No files found with $LAST_VERSION"
          grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g"
          grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied"
      - name: Add files for signing
        if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
        run: |
          echo ":"
          git add .
      - name: Signing files
        if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            // TODO: Move this to a script file.
            // We currently can't call scripts from the repo because
            // at this point in the workflow, we're on the compiled
            // artifact branch (so the scripts don't exist).
            // We can fix this with a composite action in the main repo.
            // This script is duplicated above.
            const fs = require('fs');
            const crypto = require('crypto');
            const {execSync} = require('child_process');

            // TODO: when we move this to a script, we can use this from npm.
            // Copy of signedsource since we can't install deps on this branch.
            const GENERATED = '@' + 'generated';
            const NEWTOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>';
            const PATTERN = new RegExp(`${GENERATED} (?:SignedSource<<([a-f0-9]{32})>>)`);

            const TokenNotFoundError = new Error(
              `SignedSource.signFile(...): Cannot sign file without token: ${NEWTOKEN}`
            );

            function hash(data, encoding) {
              const md5sum = crypto.createHash('md5');
              md5sum.update(data, encoding);
              return md5sum.digest('hex');
            }

            const SignedSource = {
              getSigningToken() {
                return `${GENERATED} ${NEWTOKEN}`;
              },
              isSigned(data) {
                return PATTERN.exec(data) != null;
              },
              signFile(data) {
                if (!data.includes(NEWTOKEN)) {
                  if (SignedSource.isSigned(data)) {
                    // Signing a file that was previously signed.
                   data = data.replace(PATTERN, SignedSource.getSigningToken());
                  } else {
                    throw TokenNotFoundError;
                  }
                }
                return data.replace(NEWTOKEN, `SignedSource<<${hash(data, 'utf8')}>>`);
              },
            };

            const directory = './compiled-rn';
            console.log('Signing files in directory:', directory);
            try {
              const result = execSync(`git status --porcelain ${directory}`, {encoding: 'utf8'});
              console.log(result);

              // Parse the git status output to get file paths!
              const files = result.split('\n').filter(file => file.endsWith('.js'));

              if (files.length === 0) {
                throw new Error(
                  'git status returned no files to sign. this job should not have run.'
                );
              } else {
                files.forEach(line => {
                  let file = null;
                  if (line.startsWith('D ')) {
                    return;
                  } else if (line.startsWith('R ')) {
                    file = line.slice(line.indexOf('->') + 3);
                  } else {
                    file = line.slice(3).trim();
                  }
                  if (file) {
                    console.log('  Signing file:', file);
                    const originalContents = fs.readFileSync(file, 'utf8');
                    const signedContents = SignedSource.signFile(
                      originalContents
                        // Need to add the header in, since it's not inserted at build time.
                        .replace(' */\n', ` * ${SignedSource.getSigningToken()}\n */\n`)
                    );

                    fs.writeFileSync(file, signedContents, 'utf8');
                  }
                });
              }
            } catch (e) {
              process.exitCode = 1;
              console.error('Error signing files:', e);
            }
      - name: Will commit these changes
        if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
        run: |
          git add .
          git status
      - name: Check commit message
        if: inputs.dry_run
        run: |
          git fetch origin --quiet
          git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:"%B"
      - name: Commit changes to branch
        if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
        run: |
          git config --global user.email "${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}"
          git config --global user.name "${{ github.triggering_actor }}"

          git fetch origin --quiet
          git commit -m "$(git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:'%B%n%nDiffTrain build for [${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha}})')" || echo "No changes to commit"
      - name: Push changes to branch
        if: inputs.dry_run == false && (inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true')
        run: git push
