Azure / static-web-apps

Azure Static Web Apps. For bugs and feature requests, please create an issue in this repo. For community discussions, latest updates, kindly refer to the Discussions Tab. To know what's new in Static Web Apps, visit https://aka.ms/swa/ThisMonth
https://aka.ms/swa
MIT License
323 stars 55 forks source link

"Incrementally" deploy the Azure Static Web app #448

Open plamber opened 3 years ago

plamber commented 3 years ago

Hello, we are using the Azure Static Web Apps in combination with React Apps. Like most of the React Applications we are using code splitting to bundle our resources.

The default webpack configuration in Create React App (and best practice) is to hash your assets.

main.[hash].chunk.js
1.[hash].chunk.js
2.[hash].chunk.js

Each of your JS and CSS files will have a unique hash appended to the filename that is generated, which allows you to use aggressive caching techniques to avoid the browser re-downloading your assets if the file contents haven’t changed.

If I am not mistaken when publishing a new version to the Azure Static Web app old deployed assets are being replaced by the new assets bringing up these new issue.

I would like to ask you what your recommendations and best practices are to tackle this problem? Do you think it might be possible to have an option to "incrementally" upgrade an app during deployment? My thought was to keep a copy of the current version residing on the Azure Web App and "just" override the files that have been changed, keeping the old files and not deleting them from the folder. There shouldn't be a big overhead over time. On the other hand, it will ensure that users are not facing such issues due to caching.

Alternatively, we would also be happy to have the opportunity to directly access the filesystem of the Static Web App and handle these deployment scenarios by our own.

Thank you very much for your feedback, Patrick

miwebst commented 3 years ago

Hey @plamber, this is a really interesting suggestion and definitely an improvement to the service. Let me sync with the team and decide on a path forward for enabling this!

plamber commented 3 years ago

Thank you @miwebst. Such capability would simplify our deployment strategy a lot.

Today we solved it by having a blob storage where we are pushing the files we are building. Afterwards we download the whole folder to our pipeline and push the contents to the Azure Static Web App.

The solution works but has a lot of moving parts that might break.

miwebst commented 3 years ago

So upon thinking about this further, I think there are 2 layers to this:

  1. On content update, we currently change all etags even when we don't change content. This is wrong and forces clients to refresh their cache unnecessarily
  2. On content update, we treat the update as atomic and remove any files that are no longer 'needed' after some time. We determine that a file is no longer needed if it is not present in the upload from the most recent deployment.

Is it correct to say that 2 is the issue that is affecting your app deployment? I think I was thinking 1 upon initial reading but after re-reading it seems more likely that you'd like for these old files to hang around for old clients.

plamber commented 3 years ago

Hi @miwebst,

Issue 1

Is not directly affecting our scenario. The only drawback I see is increase of network bandwidth if you are deploying many times.

Changing the ETAGs is certainly a good idea for Frameworks that require a cache invalidation and do not handle it by their own like in React. In the classic React setup you have a sort of "guaranteed" invalidation based on filenames and the naming convention. The index.html will reference to the newly generated files and also be different compared to the previous index.html if something changed.

Issue 2

Affecting our deployment.

By removing the "no longer needed" files you are breaking all applications referencing to old CSS and JS files. Keeping the existing files in the directory and just overwriting what has been changed would solve all our issues.

User with old index.html

The user has still an old version of the index.html (maybe for a couple of minutes, hours, days). The code will reference the old files until the index.html is being updated.

User getting the new index.html

The application will run without issues because it is referencing the new files.

Does it make sense for you?

Cheers

plamber commented 3 years ago

@miwebst

Something came into my mind that might be a nice addition to the issue 2.

When keeping the old files and uploading new ones you might come to the situation of having a lot of "garbage" files lying on your Static Web App. You could also solve this by deleting files that are older than x-days from the storage.

Let us imagine I am doing continuous deployments every day. This might add few changing files every day. The clients will certainly update their cache after a couple of days or so. There is no reason of keeping the "old" files on the storage. Having the option to tell you, upload these files and delete all files with a modify date older than x-days could solve this problem.

miwebst commented 3 years ago

Yes I think this ask makes sense. We will need to think more about supporting this scenario. Just curious, what would be your expectation for allowing old clients to refresh? 1-day, 7-days, 30-days?

plamber commented 3 years ago

Hi, Would rather go to 30, 60, or oven 90d. Maybe keeping it 30d default with the possibility to pass you a numeric value in the configuration. Maybe also passing -1 meaning that it should not delete the files at all.

I am thinking of users that are not reguraly visiting the site. Having just 7d is not fixing the problem.

Cheers

miwebst commented 3 years ago

@anthonychu may be able to provide some guidance on available options here.

anthonychu commented 3 years ago

I think this is better solved by using built-in capabilities in frameworks like error boundaries in React to catch the exceptions when the bundles have disappeared and act accordingly, such as reloading the app.

This should only be an issue when an app is open and you navigate to a route that attempts to download a bundle that's already gone. /index.html shouldn't have a long cache expiry, so folks returning to an app should get an updated version.

plamber commented 3 years ago

Hi @anthonychu, after all the issues we encountered in the Teams client we implemented an error boundary as suggested in this post.

Do you have some other suggestions how to ensure that this becomes failsafe?

Thank you for your feedback, Patrick

tribalsarthur commented 3 years ago

We are considering using a static web app to host some html reports from tools like JMeter and Cucumber so that we can securely and conveniently share the output of build pipelines with testers.

It would be really helpful to be able to upload new static content without removing the existing content.

Otherwise each pipeline has to download all the other reports that should still be on the site and add its own before uploading. This could have concurrency problems if several pipelines complete at the same time.

anthonychu commented 3 years ago

@tribalsarthur Can you use a separate branch/repo for the static web app? Your pipelines can use git to pull, add the files, and push to deploy new test results.

andyblack19 commented 2 years ago

I think this is better solved by using built-in capabilities in frameworks like error boundaries in React to catch the exceptions when the bundles have disappeared and act accordingly, such as reloading the app.

This should only be an issue when an app is open and you navigate to a route that attempts to download a bundle that's already gone. /index.html shouldn't have a long cache expiry, so folks returning to an app should get an updated version.

This is not a great solution for single-page apps, where the /index.html page might be in use for a long period of time. I'd have expected that 'previous' static files would remain in the edge node cache for a configurable amount of time, where they're not included in a subsequent deployment.

I think we're going to have to get around this by putting Front Door in front of the ASWA, in which case the files might as well have been hosted in a regular Storage Account.

plamber commented 2 years ago

@andyblack19 and @anthonychu

We solved our problem by uploading all incremental files to a storage account to keep track of the old assets. The contents of this storage account are then pushed to the Static Web App.

This is a workaround but at least we won't have any file missing on our client applications.

krixon-pg commented 2 years ago

@miwebst, can I please clarify what you mean by this?

On content update, we treat the update as atomic

Does this mean the upload process itself is atomic, i.e. index.html will not become visible to clients before a JS file it references? Otherwise there is a potential problem where the sequence of events is:

  1. index.html is uploaded
  2. New client with empty cache requests index.html <-- error occurs here
  3. foo.js is uploaded
Supcar27 commented 3 months ago

We are currently trying to fix this exact issue with similar approach to @plamber using blob storage.

However this is really ineffecient and i feel like its begging for things to break. Having frameworks handle this really does not work for us since we have a lot of frequent deployments users can loose some data by reloading page. It would be really really cool if we can have the old files hang out ther for some time just a few days would work for us.

Supcar27 commented 3 months ago

@plamber Perhaps you can help.

I'm running into thiss issue when i switched to the deployment strategy as you mentioned using azure blob storage, the config file staticwebapp.config.json file is no longer respected and i have the issue where the root url works but when trying to access some route the browser tries to access the html which does not exist and it fails with a 404 SO question here

plamber commented 3 months ago

Hi @Supcar27, we are still using static web apps but we keep a storage account to download, update, and then re-publish the contents to the static web app.

I can share with you the GitHub action we run. Hope this helps.

name: build and deploy

on:
  workflow_call:
    inputs:
      environment:
        description: The Github Environment to run
        required: true
        type: string
      app-name: 
        description: The app name of the static web app
        required: true
        type: string
      app-location:
        description: The project location of the app
        required: true
        type: string
      app-build-command:
        description: The build command for this app
        required: true
        type: string
      azure-storage-account:
        description: Storage account to store the temporary files
        required: true
        type: string
      azure-storage-folder: 
        description: Storage account folder to store the temporary files
        required: true
        type: string
      app-build-target: 
        description: defines the location of the app
        default: "/dist"
        type: string

    secrets:
      npm-token: 
        description: The npm token to restore the packages
        required: true

env:
  CI: false

permissions:
   id-token: write
   contents: read

jobs:
  build-and-deploy:
    runs-on: 'ubuntu-latest'
    timeout-minutes: 15
    environment: 
      name: ${{ inputs.environment }}

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

      - uses: bahmutov/npm-install@v1
        with:
          working-directory: ${{ inputs.app-location }}
        env:
          GITHUB_PACKAGE_TOKEN: ${{ secrets.npm-token }}

      - name: Build
        run: ${{ inputs.app-build-command }}
        working-directory: ${{ inputs.app-location }}

      - uses: azure/login@v1
        with:
          client-id: ${{ vars.AADAPPID }}
          tenant-id: ${{ vars.TENANTID }}
          subscription-id: ${{ vars.SUBSCRIPTIONID }}

      - name: upload current build for solution merging
        run: |
          az storage blob upload-batch --overwrite --account-name ${{ inputs.azure-storage-account }} --source "${{ inputs.app-location }}${{ inputs.app-build-target }}/" --destination ${{ inputs.azure-storage-folder }}

      - name: create target download folder for solution merging
        shell: pwsh
        run: |
          New-Item "${{ inputs.app-location }}/build_final/" -ItemType Directory

      - name: download merged solution before upload
        run: |
          az storage blob download-batch --account-name ${{ inputs.azure-storage-account }} --destination "${{ inputs.app-location }}/build_final/" --pattern "*.*" --source ${{ inputs.azure-storage-folder }}

      - uses: azure/CLI@v1
        with:
          inlineScript: |
              SWA_DEPLOYMENT_TOKEN=$(az staticwebapp secrets list -n ${{ inputs.app-name }} --query 'properties.apiKey' -o tsv)
              echo "::add-mask::$SWA_DEPLOYMENT_TOKEN"
              echo SWA_DEPLOYMENT_TOKEN=$SWA_DEPLOYMENT_TOKEN >> $GITHUB_ENV   

      - name: Deploy (SPA)
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ env.SWA_DEPLOYMENT_TOKEN }}
          action: "upload"
          app_location: "${{ inputs.app-location }}/build_final/."
          skip_app_build: "true"

      - name: deletes files on blob storage that are older than 3 months
        shell: pwsh
        run: |
          $time = (Get-date).AddMonths(-3).ToString("yyyy-MM-ddT00:00Z")
          az storage blob delete-batch --account-name ${{ inputs.azure-storage-account }} -s ${{ inputs.azure-storage-folder }} --if-unmodified-since $time

Cheers