diff --git a/.env.example b/.env.example index 88405a0..0c3b636 100644 --- a/.env.example +++ b/.env.example @@ -4,13 +4,10 @@ REACT_APP_ENV_NAME=testing # REACT_APP_ENV_NAME=production # IPFS Gateway -REACT_APP_IPFS_GATEWAY=https://bosonprotocol.infura-ipfs.io/ipfs +REACT_APP_IPFS_GATEWAY=https://.mypinata.cloud/ipfs -# Infura IPFS project ID, used for auth header -REACT_APP_INFURA_IPFS_PROJECT_ID= - -# Infura IPFS project secret, used for auth header -REACT_APP_INFURA_IPFS_PROJECT_SECRET= +# Pinata JWT, used for the auth header when uploading to IPFS +REACT_APP_IPFS_JWT= # WalletConnect ProjectId REACT_APP_WALLET_CONNECT_PROJECT_ID= diff --git a/.fleek.json b/.fleek.json deleted file mode 100644 index bd459d3..0000000 --- a/.fleek.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sites": [ - { - "slug": "widgets-test", - "distDir": "build", - "buildCommand": "npm run build" - } - ] -} \ No newline at end of file diff --git a/.fleek_production.json b/.fleek_production.json deleted file mode 100644 index ba057ac..0000000 --- a/.fleek_production.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sites": [ - { - "slug": "widgets-production", - "distDir": "build", - "buildCommand": "npm run build" - } - ] -} \ No newline at end of file diff --git a/.fleek_staging.json b/.fleek_staging.json deleted file mode 100644 index cbb903d..0000000 --- a/.fleek_staging.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sites": [ - { - "slug": "widgets-staging", - "distDir": "build", - "buildCommand": "npm run build" - } - ] -} \ No newline at end of file diff --git a/.github/actions/setup-node-npm/action.yml b/.github/actions/setup-node-npm/action.yml new file mode 100644 index 0000000..c06ad44 --- /dev/null +++ b/.github/actions/setup-node-npm/action.yml @@ -0,0 +1,17 @@ +name: "Setup Node and npm" +description: >- + Installs Node at the version in .nvmrc with the npm cache restored. Requires the repository + to be checked out. Note that the `npm ci` performed by callers runs this package's + postinstall (`copy-zoid`), which generates the git-ignored public/scripts/zoid directory + the build needs - so install must always precede build. + +runs: + using: composite + steps: + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + registry-url: "https://registry.npmjs.org" + cache: "npm" + cache-dependency-path: "./package-lock.json" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10e5f20..cf8f473 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,151 +1,45 @@ -name: Call reusable workflow - Widgets +name: CI - Widgets +# On pushes to main this is invoked by deploy-testing.yaml via workflow_call, so that the +# deploy is gated on it rather than racing it. on: - release: - types: [created] pull_request: branches: - main - workflow_dispatch: + workflow_call: -jobs: - prejob: - name: Get Latest Tag - runs-on: ubuntu-22.04 - outputs: - releaseTag: ${{ steps.tag.outputs.tag }} - releaseName: ${{ steps.name.outputs.name }} - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - - uses: "WyriHaximus/github-action-get-previous-tag@v1" - id: tag - - run: echo "name=$(git describe --tags)" >> $GITHUB_OUTPUT - id: name - - call-reusable-workflow-PR: - uses: ./.github/workflows/ci_reusable.yaml - if: github.event_name == 'pull_request' - needs: prejob - with: - ENV_NAME: "staging" - REACT_APP_ENV_NAME: "staging" - DEPLOY_TO_FLEEK: false - REACT_APP_RELEASE_TAG: ${{ needs.prejob.outputs.releaseTag }} - REACT_APP_RELEASE_NAME: ${{ needs.prejob.outputs.releaseName }} - REACT_APP_META_TX_API_KEY_MAP: ${{ vars.REACT_APP_META_TX_API_KEY_MAP }} - REACT_APP_META_TX_API_IDS_MAP: ${{ vars.REACT_APP_META_TX_API_IDS_MAP }} - secrets: - FLEEK_API_KEY: ${{ secrets.FLEEK_API_KEY }} - REACT_APP_INFURA_IPFS_PROJECT_ID: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_ID }} - REACT_APP_INFURA_IPFS_PROJECT_SECRET: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_SECRET }} - REACT_APP_WALLET_CONNECT_PROJECT_ID: ${{ secrets.REACT_APP_WALLET_CONNECT_PROJECT_ID_STAGING }} - REACT_APP_MAGIC_API_KEY: ${{ secrets.REACT_APP_MAGIC_API_KEY_STAGING }} - REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY_STAGING }} - - call-reusable-workflow-staging: - uses: ./.github/workflows/ci_reusable.yaml - if: github.event_name == 'release' && github.event.action == 'created' - needs: prejob - with: - ENV_NAME: "staging" - REACT_APP_ENV_NAME: "staging" - DEPLOY_TO_FLEEK: true - REACT_APP_RELEASE_TAG: ${{ needs.prejob.outputs.releaseTag }} - REACT_APP_RELEASE_NAME: ${{ needs.prejob.outputs.releaseName }} - REACT_APP_META_TX_API_KEY_MAP: ${{ vars.REACT_APP_META_TX_API_KEY_MAP }} - REACT_APP_META_TX_API_IDS_MAP: ${{ vars.REACT_APP_META_TX_API_IDS_MAP }} - secrets: - FLEEK_API_KEY: ${{ secrets.FLEEK_API_KEY }} - REACT_APP_INFURA_IPFS_PROJECT_ID: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_ID }} - REACT_APP_INFURA_IPFS_PROJECT_SECRET: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_SECRET }} - REACT_APP_WALLET_CONNECT_PROJECT_ID: ${{ secrets.REACT_APP_WALLET_CONNECT_PROJECT_ID_STAGING }} - REACT_APP_MAGIC_API_KEY: ${{ secrets.REACT_APP_MAGIC_API_KEY_STAGING }} - REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY_STAGING }} - FLEEK_TOKEN: ${{ secrets.FLEEK_TOKEN }} - FLEEK_PROJECT_ID: ${{ secrets.FLEEK_PROJECT_ID }} +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} - call-reusable-workflow-production: - uses: ./.github/workflows/ci_reusable.yaml - if: github.event_name == 'workflow_dispatch' - needs: prejob - with: - ENV_NAME: "production" - REACT_APP_ENV_NAME: "production" - DEPLOY_TO_FLEEK: true - REACT_APP_RELEASE_TAG: ${{ needs.prejob.outputs.releaseTag }} - REACT_APP_RELEASE_NAME: ${{ needs.prejob.outputs.releaseName }} - REACT_APP_META_TX_API_KEY_MAP: ${{ vars.REACT_APP_META_TX_API_KEY_MAP }} - REACT_APP_META_TX_API_IDS_MAP: ${{ vars.REACT_APP_META_TX_API_IDS_MAP }} - secrets: - FLEEK_API_KEY: ${{ secrets.FLEEK_API_KEY }} - REACT_APP_INFURA_IPFS_PROJECT_ID: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_ID }} - REACT_APP_INFURA_IPFS_PROJECT_SECRET: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_SECRET }} - REACT_APP_WALLET_CONNECT_PROJECT_ID: ${{ secrets.REACT_APP_WALLET_CONNECT_PROJECT_ID_PRODUCTION }} - REACT_APP_MAGIC_API_KEY: ${{ secrets.REACT_APP_MAGIC_API_KEY_PRODUCTION }} - REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY_PRODUCTION }} - FLEEK_TOKEN: ${{ secrets.FLEEK_TOKEN }} - FLEEK_PROJECT_ID: ${{ secrets.FLEEK_PROJECT_ID }} - - job-summary: - name: Create Job Summary +jobs: + checks: + name: Format, lint, types and build runs-on: ubuntu-22.04 - if: ${{ always() && contains(join(needs.*.result, ','), 'success') }} - needs: - [ - call-reusable-workflow-staging, - call-reusable-workflow-production - ] + permissions: + contents: read steps: - - uses: actions/checkout@v3 - - name: Install JQ - uses: r26d/jq-action@master - with: - cmd: jq -n env - - name: Get job outputs + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-node-npm + + - run: npm ci + + # Non-mutating checks: `npm run prettier` and `npm run lint` rewrite files, which would + # make them pass unconditionally and would leave the build compiling something other + # than the committed code. + - run: npm run prettier:check + - run: npm run lint:check + - run: npm run tsc + + # Every other path already builds this commit against the real environment values - + # deploy-preview.yaml for a PR, deploy-testing.yaml for a push to main - and a deploy + # that fails to build publishes nothing. Repeating it here would spend a second full + # build on the same tree. Fork PRs get no preview deployment (see the `if:` in + # deploy-preview.yaml), so they are built here. No env vars are needed: CRA compiles + # src/config.ts without executing it. + - name: Build (fork PRs only) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} env: - OUTPUTS: ${{ toJSON(needs) }} - run: | - succeededJobOutput="$( - echo '${{ env.OUTPUTS }}' | jq '.[] | select(.result | startswith("success")) | .outputs' - )" - echo 'JOB_OUTPUT='$succeededJobOutput >> $GITHUB_ENV - - name: Get job vars - run: | - jobLint="$( - echo '${{ env.JOB_OUTPUT }}' | - jq '.JOB_LINT' - )" - echo 'JOB_LINT='$jobLint >> $GITHUB_ENV - - jobBuild="$( - echo '${{ env.JOB_OUTPUT }}' | - jq '.JOB_BUILD' - )" - echo 'JOB_BUILD='$jobBuild >> $GITHUB_ENV - - deployUrl="$( - echo '${{ env.JOB_OUTPUT }}' | - jq '.DEPLOY_URL' - )" - echo 'DEPLOY_URL='$deployUrl >> $GITHUB_ENV - - - name: Adding Job Summary - run: | - echo "| Command | Status |" >> $GITHUB_STEP_SUMMARY - echo "| ---- | ---- |" >> $GITHUB_STEP_SUMMARY - if [ ${{ env.JOB_LINT }} == success ]; then - echo "| Lint | ✅ |" >> $GITHUB_STEP_SUMMARY - else - echo "| Lint | ❌ |" >> $GITHUB_STEP_SUMMARY - fi - if [ ${{ env.JOB_BUILD }} == success ]; then - echo "| Build | ✅ |" >> $GITHUB_STEP_SUMMARY - else - echo "| Build | ❌ |" >> $GITHUB_STEP_SUMMARY - fi - - name: Adding Job Summary Deploy Url - if: ${{ env.DEPLOY_URL }} - run: echo "| URL | ${{ env.DEPLOY_URL }} |" >> $GITHUB_STEP_SUMMARY + GENERATE_SOURCEMAP: "false" + run: npm run build diff --git a/.github/workflows/ci_reusable.yaml b/.github/workflows/ci_reusable.yaml deleted file mode 100644 index 970d7fd..0000000 --- a/.github/workflows/ci_reusable.yaml +++ /dev/null @@ -1,119 +0,0 @@ -name: Build, Test and Deploy the Hosted Widgets - Reusable workflow - -on: - workflow_call: - inputs: - ENV_NAME: - required: true - type: string - REACT_APP_ENV_NAME: - required: true - type: string - DEPLOY_TO_FLEEK: - required: true - type: boolean - REACT_APP_RELEASE_TAG: - required: false - type: string - REACT_APP_RELEASE_NAME: - required: false - type: string - REACT_APP_META_TX_API_KEY_MAP: - required: true - type: string - REACT_APP_META_TX_API_IDS_MAP: - required: true - type: string - secrets: - FLEEK_API_KEY: - required: false - REACT_APP_INFURA_IPFS_PROJECT_ID: - required: false - REACT_APP_INFURA_IPFS_PROJECT_SECRET: - required: false - REACT_APP_WALLET_CONNECT_PROJECT_ID: - required: true - REACT_APP_MAGIC_API_KEY: - required: true - REACT_APP_INFURA_KEY: - required: true - FLEEK_TOKEN: - required: false - FLEEK_PROJECT_ID: - required: false - outputs: - JOB_LINT: - value: ${{ jobs.build-test-deploy.outputs.jobLint }} - JOB_BUILD: - value: ${{ jobs.build-test-deploy.outputs.jobBuild }} - DEPLOY_URL: - value: ${{ jobs.build-test-deploy.outputs.deployUrl }} - -jobs: - build-test-deploy: - name: Build, Test and Deploy the Hosted Widgets - runs-on: ubuntu-22.04 - env: - REACT_APP_ENV_NAME: ${{ inputs.REACT_APP_ENV_NAME }} - REACT_APP_RELEASE_TAG: ${{ inputs.REACT_APP_RELEASE_TAG }} - REACT_APP_RELEASE_NAME: ${{ inputs.REACT_APP_RELEASE_NAME }} - REACT_APP_INFURA_IPFS_PROJECT_ID: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_ID }} - REACT_APP_INFURA_IPFS_PROJECT_SECRET: ${{ secrets.REACT_APP_INFURA_IPFS_PROJECT_SECRET }} - REACT_APP_WALLET_CONNECT_PROJECT_ID: ${{ secrets.REACT_APP_WALLET_CONNECT_PROJECT_ID }} - REACT_APP_META_TX_API_KEY_MAP: ${{ inputs.REACT_APP_META_TX_API_KEY_MAP }} - REACT_APP_META_TX_API_IDS_MAP: ${{ inputs.REACT_APP_META_TX_API_IDS_MAP }} - REACT_APP_DEFAULT_DISPUTE_PERIOD_DAYS: "30" - REACT_APP_DEFAULT_RESOLUTION_PERIOD_DAYS: "15" - REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_TESTING: "https://interface-a9d.pages.dev/#/chat/{id}" - REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_STAGING: "https://boson-dapp-staging.pages.dev/#/chat/{id}" - REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_PRODUCTION: "https://bosonapp.io/#/chat/{id}" - REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_TESTING: "https://boson-dr-center-testing.pages.dev/#/exchange/{id}/raise-dispute" - REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_STAGING: "https://boson-dr-center-staging.pages.dev/#/exchange/{id}/raise-dispute" - REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_PRODUCTION: "https://disputes.bosonprotocol.io/#/exchange/{id}/raise-dispute" - REACT_APP_IPFS_GATEWAY: "https://bosonprotocol.infura-ipfs.io/ipfs" - REACT_APP_BUYER_SELLER_AGREEMENT_TEMPLATE: "ipfs://QmaNj7vGuCEvaM5vyucp5z1S9VprMnZWmVxYGn6FHhgePF" - REACT_APP_RNFT_LICENSE_TEMPLATE: "ipfs://QmdN6tCBkiGiCyr7xEfsEysoYrdkacdU3P2mEdVC5y4nF2" - REACT_APP_FAIR_EXCHANGE_POLICY_RULES: "ipfs://QmbVmAspK2wi6Xrm6yB75TLqgfv9PAVv3kWxdsyQpEm79D" - REACT_APP_MAGIC_API_KEY: ${{ secrets.REACT_APP_MAGIC_API_KEY }} - REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY }} - outputs: - jobLint: ${{ steps.lint.outputs.pass }} - jobBuild: ${{ steps.build.outputs.pass }} - deployUrl: ${{ steps.deploy.outputs.deployUrl }} - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - - uses: actions/setup-node@v3 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - cache: "npm" - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: npm-${{ hashFiles('package-lock.json') }} - restore-keys: npm- - - run: npm ci - - run: npm run prettier - - run: npm run lint - - id: lint - run: echo "::set-output name=pass::success" - - run: npm run build - - id: build - run: echo "::set-output name=pass::success" - - name: (if 'staging' env) Copy contents from .fleek_staging.json to .fleek.json - if: ${{ inputs.ENV_NAME == 'staging' }} - run: mv .fleek_staging.json .fleek.json - - name: (if 'production' env) Copy contents from .fleek_production.json to .fleek.json - if: ${{ inputs.ENV_NAME == 'production' }} - run: mv .fleek_production.json .fleek.json - - name: Deploy via fleek - if: ${{ inputs.DEPLOY_TO_FLEEK }} - id: deploy - run: npx @fleek-platform/cli sites deploy --config ./.fleek.json - env: - FLEEK_TOKEN: ${{ secrets.FLEEK_TOKEN }} - FLEEK_PROJECT_ID: ${{ secrets.FLEEK_PROJECT_ID }} diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml new file mode 100644 index 0000000..2dd5990 --- /dev/null +++ b/.github/workflows/deploy-preview.yaml @@ -0,0 +1,53 @@ +name: Deploy PR preview + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened] + +jobs: + deploy: + # This is also the security boundary for the secrets below: a fork PR never reaches this + # job, so only code from a branch in this repository is ever built with the Cloudflare + # token on the runner. Fork PRs cannot read environment secrets anyway, so they would fail + # the required-configuration check rather than deploy. + if: github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/workflows/deploy_reusable.yaml + with: + environment: testing + # Anything other than the project's production branch makes this a Preview deployment, + # published at https://pr-..pages.dev. + cf_branch: pr-${{ github.event.number }} + is_preview: true + # `inherit`, not an explicit allow-list: the REACT_APP_* values are scoped to the `testing` + # GitHub Environment and are resolved by the called job's `environment:`. Naming them here + # would pass this workflow's own repository-scope values instead, which for an + # environment-scoped secret is the empty string - a green deploy of a bundle with no Magic + # key and no WalletConnect id. + secrets: inherit + + comment: + needs: deploy + # deployment_url comes from the deploy job's output. It should always be set, but if it + # ever is not, post nothing rather than a comment whose links are bare relative paths. + if: ${{ needs.deploy.outputs.deployment_url != '' }} + runs-on: ubuntu-22.04 + permissions: + pull-requests: write + steps: + - name: Comment preview URL + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: cf-preview + message: | + ### Preview deployment + + | Widget | URL | + | ---- | --- | + | Dev playground | ${{ needs.deploy.outputs.deployment_url }}/#/dev | + | Redemption | ${{ needs.deploy.outputs.deployment_url }}/#/redeem | + | Finance | ${{ needs.deploy.outputs.deployment_url }}/#/finance | + | Embedding example | ${{ needs.deploy.outputs.deployment_url }}/example.html | + + Built from ${{ github.event.pull_request.head.sha }} against the `testing` environment. diff --git a/.github/workflows/deploy-production.yaml b/.github/workflows/deploy-production.yaml new file mode 100644 index 0000000..043a1b8 --- /dev/null +++ b/.github/workflows/deploy-production.yaml @@ -0,0 +1,57 @@ +name: Deploy to production + +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag to deploy (e.g. v0.0.50)" + required: true + type: string + +jobs: + verify-tag: + name: Verify tag exists + runs-on: ubuntu-22.04 + permissions: + contents: read + steps: + - name: Resolve tag + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + + # The tag is free-form text from the dispatch form and ends up on a wrangler command + # line in deploy_reusable.yaml. That step validates it too; rejecting it here means a + # bad value fails in seconds instead of after a full build. + case "$TAG" in + "" | *[!A-Za-z0-9._/+-]*) + echo "::error::Tag '${TAG}' must be non-empty and use only A-Z a-z 0-9 . _ / + -" + exit 1 + ;; + esac + + # One API call rather than cloning every commit, branch and tag of the repository + # onto a runner to answer a yes/no question. The deploy job resolves the commit it + # builds from its own checkout, so nothing needs to be handed on from here. + if ! object="$(gh api "repos/${REPO}/git/ref/tags/${TAG}" --jq .object.sha 2>/dev/null)"; then + echo "::error::Tag '${TAG}' does not exist in this repository." + exit 1 + fi + echo "Tag ${TAG} exists (${object})." + + deploy: + needs: verify-tag + uses: ./.github/workflows/deploy_reusable.yaml + with: + environment: production + ref: ${{ inputs.tag }} + release_tag: ${{ inputs.tag }} + # Must equal the production project's production branch, or this lands as a Preview + # deployment and widgets.bosonprotocol.io keeps serving the previous build. + cf_branch: main + # See the note in deploy-preview.yaml: the REACT_APP_* values are environment-scoped and + # an explicit allow-list would pass empty repository-scope values instead. + secrets: inherit diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml new file mode 100644 index 0000000..8c8cb0a --- /dev/null +++ b/.github/workflows/deploy-staging.yaml @@ -0,0 +1,21 @@ +name: Deploy to staging + +# `published` rather than `created`: `created` does not fire when a release is published from +# a draft. +on: + release: + types: [published] + +jobs: + deploy: + uses: ./.github/workflows/deploy_reusable.yaml + with: + environment: staging + # Build the tagged commit itself, so staging matches the release even if main has moved + # on since the tag was cut. + ref: ${{ github.event.release.tag_name }} + release_tag: ${{ github.event.release.tag_name }} + # Must equal the staging project's production branch, or this lands as a Preview + # deployment and boson-widgets-staging.pages.dev keeps serving the previous build. + cf_branch: main + secrets: inherit diff --git a/.github/workflows/deploy-testing.yaml b/.github/workflows/deploy-testing.yaml new file mode 100644 index 0000000..2abcf7b --- /dev/null +++ b/.github/workflows/deploy-testing.yaml @@ -0,0 +1,22 @@ +name: Deploy to testing + +on: + push: + branches: + - main + +jobs: + # Gate the deploy on the same checks a PR runs: a merge can produce a broken combination + # that neither parent had. + ci: + uses: ./.github/workflows/ci.yaml + secrets: inherit + + deploy: + needs: ci + uses: ./.github/workflows/deploy_reusable.yaml + # No `ref`: build the commit that was pushed. + with: + environment: testing + cf_branch: main + secrets: inherit diff --git a/.github/workflows/deploy_reusable.yaml b/.github/workflows/deploy_reusable.yaml new file mode 100644 index 0000000..62b40f1 --- /dev/null +++ b/.github/workflows/deploy_reusable.yaml @@ -0,0 +1,296 @@ +name: Build and deploy to Cloudflare Pages - Reusable workflow + +on: + workflow_call: + inputs: + environment: + description: "GitHub Environment supplying the scoped vars/secrets (testing|staging|production)" + required: true + type: string + ref: + description: "Git ref to build. Leave empty to use the ref that triggered the run." + required: false + type: string + default: "" + cf_branch: + description: "Cloudflare deployment branch label. Must equal the project's production branch to publish a Production deployment." + required: false + type: string + default: "main" + release_tag: + description: "Release tag stamped into the bundle and onto the Cloudflare deployment." + required: false + type: string + default: "" + is_preview: + description: "Preview build: a superseded run may be cancelled mid-flight." + required: false + type: boolean + default: false + outputs: + deployment_url: + description: "URL of the deployment." + value: ${{ jobs.build-deploy.outputs.deployment_url }} + +jobs: + build-deploy: + name: ${{ inputs.environment }} + runs-on: ubuntu-22.04 + environment: ${{ inputs.environment }} + permissions: + contents: read + outputs: + deployment_url: ${{ steps.deploy.outputs.deployment-url }} + concurrency: + group: deploy-${{ inputs.environment }}-${{ inputs.cf_branch }} + # Supersede stale preview builds, but never kill an environment deploy mid-upload. + cancel-in-progress: ${{ inputs.is_preview }} + env: + # The GitHub Environment name and the value src/config.ts expects are the same three + # strings, so there is nothing to configure here. + REACT_APP_ENV_NAME: ${{ inputs.environment }} + + # Committed, non-secret build configuration. These are deliberately kept in the + # workflow rather than in GitHub variables: here they are diffable and reviewed with + # the code that consumes them. + REACT_APP_DEFAULT_DISPUTE_PERIOD_DAYS: "30" + REACT_APP_DEFAULT_RESOLUTION_PERIOD_DAYS: "15" + REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_TESTING: "https://interface-a9d.pages.dev/#/chat/{id}" + REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_STAGING: "https://boson-dapp-staging.pages.dev/#/chat/{id}" + REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_PRODUCTION: "https://bosonapp.io/#/chat/{id}" + REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_TESTING: "https://boson-dr-center-testing.pages.dev/#/exchange/{id}/raise-dispute" + REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_STAGING: "https://boson-dr-center-staging.pages.dev/#/exchange/{id}/raise-dispute" + REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_PRODUCTION: "https://disputes.bosonprotocol.io/#/exchange/{id}/raise-dispute" + REACT_APP_BUYER_SELLER_AGREEMENT_TEMPLATE: "ipfs://QmaNj7vGuCEvaM5vyucp5z1S9VprMnZWmVxYGn6FHhgePF" + REACT_APP_RNFT_LICENSE_TEMPLATE: "ipfs://QmdN6tCBkiGiCyr7xEfsEysoYrdkacdU3P2mEdVC5y4nF2" + REACT_APP_FAIR_EXCHANGE_POLICY_RULES: "ipfs://QmbVmAspK2wi6Xrm6yB75TLqgfv9PAVv3kWxdsyQpEm79D" + + # Repository-scoped: both maps already hold every environment's entries in one JSON, + # keyed --, so there is nothing to scope per environment. + REACT_APP_META_TX_API_KEY_MAP: ${{ vars.REACT_APP_META_TX_API_KEY_MAP }} + REACT_APP_META_TX_API_IDS_MAP: ${{ vars.REACT_APP_META_TX_API_IDS_MAP }} + REACT_APP_IPFS_GATEWAY: ${{ vars.REACT_APP_IPFS_GATEWAY }} + + # Repository-scoped secrets, shared by every environment. + REACT_APP_IPFS_JWT: ${{ secrets.REACT_APP_IPFS_JWT }} + + # Environment-scoped secrets. Identical names across environments: GitHub resolves the + # environment's value and falls back to the repository-level one when unset, which is + # what removes the old _STAGING/_PRODUCTION name suffixes. + # + # REACT_APP_MAGIC_API_KEY and REACT_APP_INFURA_KEY are read by @bosonprotocol/react-kit + # rather than by src/, but CRA's DefinePlugin substitutes process.env across every + # webpack-parsed module including node_modules, so they are inlined at this build. + REACT_APP_WALLET_CONNECT_PROJECT_ID: ${{ secrets.REACT_APP_WALLET_CONNECT_PROJECT_ID }} + REACT_APP_MAGIC_API_KEY: ${{ secrets.REACT_APP_MAGIC_API_KEY }} + REACT_APP_INFURA_KEY: ${{ secrets.REACT_APP_INFURA_KEY }} + + steps: + - uses: actions/checkout@v4 + with: + # Empty ref means "the ref that triggered this run": the pushed commit, or the PR + # merge commit. Staging/production pass an explicit tag instead. + ref: ${{ inputs.ref }} + # Full history so `git describe --tags` can name untagged testing builds. + fetch-depth: 0 + + # Resolved from the checked-out tree, not from the ref this workflow came from, so a + # tag cut before this action existed cannot be deployed by re-running the workflow. + # Roll back from the Cloudflare dashboard instead, which redeploys a build that has + # already been uploaded. + - uses: ./.github/actions/setup-node-npm + + - name: Resolve deployment identity + env: + ENVIRONMENT: ${{ inputs.environment }} + INPUT_RELEASE_TAG: ${{ inputs.release_tag }} + CF_BRANCH: ${{ inputs.cf_branch }} + CF_PROJECT_INPUT: ${{ vars.CF_PROJECT }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + if [ -z "$CF_PROJECT_INPUT" ]; then + echo "::error::CF_PROJECT is not set on the '${ENVIRONMENT}' environment." \ + "Set it to the exact Cloudflare project name from 'wrangler pages project list'." + exit 1 + fi + + # RELEASE_TAG names the release a build belongs to; RELEASE_NAME names the build + # itself. Keep them distinct: a tag that moved with every commit would be a poor + # deployment label, and RELEASE_NAME is what shows up in the Cloudflare deployment + # list as the commit message. + if [ -n "$INPUT_RELEASE_TAG" ]; then + release_tag="$INPUT_RELEASE_TAG" + release_name="$INPUT_RELEASE_TAG" + else + release_tag="$(git describe --tags --abbrev=0 2>/dev/null || git rev-parse --short HEAD)" + release_name="$(git describe --tags --always)" + fi + + # The commit that was actually built. github.sha is the ref that triggered the run, + # which for a tag dispatch is the tip of the branch the workflow was launched from - + # not the tag being deployed. + commit_sha="${PR_HEAD_SHA:-$(git rev-parse HEAD)}" + + # These get spliced into the wrangler command line below, which is split into + # arguments before wrangler sees it. A space, quote or shell metacharacter would + # silently change what runs, and the release tag is free-form text typed into a + # workflow_dispatch form. + require_safe() { + case "$2" in + "" | *[!A-Za-z0-9._/+-]*) + echo "::error::$1 is '$2'; it must be non-empty and use only A-Z a-z 0-9 . _ / + -" + exit 1 + ;; + esac + } + require_safe "Cloudflare project name" "$CF_PROJECT_INPUT" + require_safe "Cloudflare branch" "$CF_BRANCH" + require_safe "Release name" "$release_name" + + { + echo "CF_PROJECT=$CF_PROJECT_INPUT" + echo "COMMIT_SHA=$commit_sha" + echo "RELEASE_NAME=$release_name" + echo "REACT_APP_RELEASE_TAG=$release_tag" + echo "REACT_APP_RELEASE_NAME=$release_name" + } >> "$GITHUB_ENV" + + - name: Check the build configuration is complete + env: + ENVIRONMENT: ${{ inputs.environment }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + set -euo pipefail + # `npm run build` only compiles src/config.ts, it never executes it - so a missing + # value is not a build failure. Every REACT_APP_* is inlined at build time, an unset + # one becomes an empty string, and src/config.ts then throws at module load *in the + # browser*: the deploy is green and the page is blank. This step is the only thing + # between a missing secret and a dead deployment. + # + # The list is not a copy of any single source - it is what a *deployment* cannot + # work without, which is four different things: + # + # 1. REACT_APP_ENV_NAME and the non-optional entries of the EnvVariables array in + # src/config.ts. These throw at module load, so an empty one is a blank page. + # 2. The two REACT_APP_META_TX_* maps. src/config.ts marks them `optional: true`, + # so they do not throw - getMetaTxConfig() just returns empty strings and + # gasless transactions are silently dead. Required here because shipping a + # deployment without them is not an outcome anyone wants by default. + # 3. REACT_APP_MAGIC_API_KEY and REACT_APP_INFURA_KEY. These appear nowhere in + # src/; @bosonprotocol/react-kit reads them and CRA's DefinePlugin inlines + # them at this build. Empty means email login and RPC access are dead, again + # without anything throwing. + # 4. CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, which are not app config at + # all - checked here so a missing credential fails the run in seconds instead + # of after a full build. + # + # So: adding a non-optional entry to EnvVariables means adding it here, but the + # reverse does not hold, and category 2-4 entries have to be judged on whether the + # deployed app is usable without them. + env_suffix="$(printf '%s' "$REACT_APP_ENV_NAME" | tr '[:lower:]' '[:upper:]')" + missing="" + for name in \ + REACT_APP_ENV_NAME \ + "REACT_APP_CONTACT_SELLER_FOR_EXCHANGE_${env_suffix}" \ + "REACT_APP_RAISE_DISPUTE_FOR_EXCHANGE_${env_suffix}" \ + REACT_APP_DEFAULT_DISPUTE_PERIOD_DAYS \ + REACT_APP_DEFAULT_RESOLUTION_PERIOD_DAYS \ + REACT_APP_BUYER_SELLER_AGREEMENT_TEMPLATE \ + REACT_APP_RNFT_LICENSE_TEMPLATE \ + REACT_APP_FAIR_EXCHANGE_POLICY_RULES \ + REACT_APP_WALLET_CONNECT_PROJECT_ID \ + REACT_APP_IPFS_GATEWAY \ + REACT_APP_META_TX_API_KEY_MAP \ + REACT_APP_META_TX_API_IDS_MAP \ + REACT_APP_MAGIC_API_KEY \ + REACT_APP_INFURA_KEY \ + CLOUDFLARE_API_TOKEN \ + CLOUDFLARE_ACCOUNT_ID; do + if [ -z "${!name:-}" ]; then + missing="$missing $name" + fi + done + if [ -n "$missing" ]; then + echo "::error::Missing required build configuration for environment '${ENVIRONMENT}':$missing" + exit 1 + fi + + # A value src/config.ts does not recognise is as fatal as an empty one, and the + # emptiness check above does not catch it. 'local' is accepted by config.ts but is + # never a deploy target. + case "$REACT_APP_ENV_NAME" in + testing | staging | production) ;; + *) + echo "::error::REACT_APP_ENV_NAME is '${REACT_APP_ENV_NAME}' on the '${ENVIRONMENT}' environment;" \ + "a deployment must be testing, staging or production." + exit 1 + ;; + esac + + - run: npm ci + + - name: Build + env: + # Cloudflare Pages rejects any file over 25 MiB. Nothing reads the maps - the + # deployments this workflow replaces were already built without them - and + # generating them would only publish the full source alongside the bundle. + GENERATE_SOURCEMAP: "false" + run: npm run build + + - name: Deploy to Cloudflare Pages + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: "4.129.0" + # cf_branch must equal the project's production branch to publish a Production + # deployment; anything else (pr-123) lands as a Preview. Every value interpolated + # into this command line is validated in "Resolve deployment identity" above. + command: >- + pages deploy build + --project-name=${{ env.CF_PROJECT }} + --branch=${{ inputs.cf_branch }} + --commit-hash=${{ env.COMMIT_SHA }} + --commit-message=${{ env.RELEASE_NAME }} + + - name: Smoke check the deployment + env: + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} + run: | + set -euo pipefail + # This runs against a deployment that is already serving traffic, and what it fetches + # is an SPA shell: a build that throws in config.ts still returns 200 with a + # non-empty body, because the crash happens in the browser. So it catches a failed or + # empty upload, not a broken build - keeping a broken build from shipping is what the + # configuration check before `npm run build` is for. + if [ -z "$DEPLOY_URL" ]; then + echo "::error::wrangler returned no deployment URL." + exit 1 + fi + body="$(curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors "$DEPLOY_URL")" + if [ -z "$body" ]; then + echo "::error::$DEPLOY_URL served an empty response." + exit 1 + fi + + - name: Job summary + if: ${{ always() && steps.deploy.outcome == 'success' }} + env: + ENVIRONMENT: ${{ inputs.environment }} + CF_BRANCH: ${{ inputs.cf_branch }} + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} + run: | + { + echo "| Field | Value |" + echo "| ---- | ---- |" + echo "| Environment | ${ENVIRONMENT} |" + echo "| CF project | ${CF_PROJECT} |" + echo "| CF branch | \`${CF_BRANCH}\` |" + echo "| Release | ${RELEASE_NAME} |" + echo "| Commit | ${COMMIT_SHA} |" + echo "| URL | ${DEPLOY_URL} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/README.md b/README.md index a79be3b..72b108a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,50 @@ - Finance Widget - [Local development](docs/local-development.md) +## Environments + +Each environment is built by GitHub Actions and uploaded to a Cloudflare Pages project with +`wrangler pages deploy`. Cloudflare builds nothing — every `REACT_APP_*` value is inlined into +the bundle at build time, so the projects' dashboard build settings and environment variables +are not used. + +| Env | Networks | Cloudflare project | URL | +| ---------- | ----------------- | ----------------------- | ----------------------------------------- | +| testing | base sepolia, sepolia | `boson-widgets-testing` | https://boson-widgets-testing.pages.dev/ | +| staging | base sepolia, sepolia | `boson-widgets-staging` | https://boson-widgets-staging.pages.dev/ | +| production | base, ethereum | `boson-widgets` | https://widgets.bosonprotocol.io/ | + +Deployments are triggered as follows: + +| Env | Trigger | +| ---------- | ----------------------------------------------------------------- | +| preview | Every pull request, published at `pr-.boson-widgets-testing.pages.dev` | +| testing | Every push to `main`, gated on the CI checks | +| staging | Publishing a GitHub Release (deploys that tag) | +| production | Manually running the **Deploy to production** workflow with a tag | + +Build-time configuration lives in two places. Non-secret values — the IPFS gateway, the +`ipfs://` templates, the dispute periods and the per-environment dApp and DR Center URLs — are +committed in [`deploy_reusable.yaml`](.github/workflows/deploy_reusable.yaml), where they are +diffable and reviewed alongside the code that reads them. Secrets are held by the `testing`, +`staging` and `production` GitHub Environments, under names with no environment suffix; GitHub +resolves the environment's value and falls back to the repository-level one when unset. + +Each environment also needs a `CF_PROJECT` variable holding the exact Cloudflare project name +(`wrangler pages project list`), plus the repository secrets `CLOUDFLARE_API_TOKEN` and +`CLOUDFLARE_ACCOUNT_ID`. + +An unset `REACT_APP_*` is not a build error — it becomes an empty string, and +[`src/config.ts`](src/config.ts) then throws at module load *in the browser*, so the deploy is +green and the page is blank. The **Check the build configuration is complete** step in +`deploy_reusable.yaml` lists the values a deployment cannot work without and fails the run +before building if any is missing. Add to that list when you add a variable the app depends +on. + +Branch protection on `main` should require the **Format, lint, types and build** job of +**CI - Widgets**. Required checks are matched by name, so renaming either the workflow or the +job detaches the rule silently. + ## Contributing We welcome contributions! Until now, Boson Protocol has been largely worked on by a small dedicated team. However, the ultimate goal is for all of the Boson Protocol repositories to be fully owned by the community and contributors. Issues, pull requests, suggestions, and any sort of involvement are more than welcome. diff --git a/docs/commit-widget.md b/docs/commit-widget.md index dfeeda8..7f116a6 100644 --- a/docs/commit-widget.md +++ b/docs/commit-widget.md @@ -21,11 +21,11 @@ To integrate the Boson Commit Widget, all a seller needs to do is: 2. The Seller then needs to create a button with the fragment identifier *id="boson-commit"*. When clicked, the commit modal will popup on the Seller's website. ``` - + ``` The button shall be parameterized with the following arguments: -- ```data-config-id```: specifies the Boson Configuration addressed by the widget (here ***production-137-0*** is the production configuration deployed on the Polygon blockchain). See [Boson Environment](./boson-environments.md) to get more details. +- ```data-config-id```: specifies the Boson Configuration addressed by the widget (here ***production-8453-0*** is the production configuration deployed on the Base blockchain). See [Boson Environment](./boson-environments.md) to get more details. - ```data-seller-id```: specifies the Boson Seller ID that publishes the Product being offered with the plugin - ```data-product-uuid```: specifies the ProductUUID of the Product being offered with the plugin @@ -67,7 +67,7 @@ In which case the page: - does not need to include any specific line (like the ```