fix: stop docs deploys from deleting assets live pages still load - #915
Merged
Merged
Conversation
Every deploy ran reggionick/s3-deploy with delete-removed: upload the new build, then delete every object it did not contain. Docusaurus names each JS/CSS chunk by content hash, so the previous build's chunks vanished the moment a deploy ran, and anything still holding the previous HTML - an open tab, a cached page, or a second deploy running at the same time - requested chunks that no longer existed and never hydrated. Two merges 17s apart (#912, #913) did exactly that to production: the pages rendered but no control on them worked, including the macOS tab on /docs/server/installation/. The action's cache input was also word-split into the malformed header "Cache-Control: max-age=public," on every object. - main.yml: one deploy at a time (concurrency group, never cancelling a deploy in flight), and each run deploys main as it is when it starts, so a queued, cancelled or re-run deploy can never ship an older main. - .github/scripts/deploy-docs.sh replaces the action. Hashed assets go up first and are never deleted in the same pass; a per-deploy manifest is written before any page; static files, text and pages follow, pages last; CloudFront is invalidated, dropped static files are deleted once it has landed, and the deploy is marked complete. Assets are pruned only once every page that used them was replaced more than 14 days ago, counted from the newest completed deploy, so a half-failed deploy can never cut the window short. - Correct cache headers per class: immutable for hashed assets, revalidate for pages and text, one hour for other static files; UTF-8 charset on .txt and .md (llms.txt, llms-full.txt, page copies). - .github/scripts/deploy-docs-test.sh deploys the PR's real build into a local S3 (moto) on every PR and replays the incident and the weeks after it: a deploy that dies once its pages are up, >1000-key prunes, every asset a live or still-open page loads, and the step order. Signed-off-by: slayerjain <shubhamkjain@outlook.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
keploy.io/docspages rendered but nothing on them was interactive. For example, the macOS tab on/docs/server/installation/didn't respond to clicks. The HTML being served referenced aruntime~main.<hash>.jsthat had already been deleted from the bucket, so React never hydrated.Root cause: the deploy (
reggionick/s3-deploywithdelete-removed) uploads the new build and then deletes every object the new build doesn't contain. Docusaurus names every JS/CSS chunk by content hash, so each deploy deleted the previous build's chunks straight away. Anything still using the previous HTML then broke: an open tab on its next click, a cached page, or a second deploy running concurrently. Two merges 17 s apart (#912, #913) triggered the concurrent case in production. Production was restored by re-running the latest deploy; this PR fixes the cause.The action's
cache:input was also word-split into a malformedCache-Control: max-age=public,on every object (visible live withcurl -I).Fix
main.ymlconcurrencygroup so only one deploy runs at a time; a deploy already running is never cancelled.actions/checkoutwithref: main, so a run deploysmainas it is when the run starts. A queued, cancelled or re-run deploy can never ship an oldermainthan the one before it..github/scripts/deploy-docs.shreplaces the action:docs/assets/go up first,immutable, and are never deleted in the same pass.docs/_deploy/asset-manifests/) before any page..txt/.md, then pages last.Cache headers per class:
Cache-Controlmax-age=31536000, immutablemax-age=0, s-maxage=86400, must-revalidatemax-age=3600.txtand.md(llms.txt,llms-full.txt, the page copies) are also served withcharset=utf-8, which the aws CLI otherwise drops..github/scripts/deploy-docs-test.sh, run inbuild_and_check.ymlon every PR, deploys that PR's real build into a local S3 and CloudFront (moto). It replays the incident and the weeks after it:a deploy that dies once its pages are up, whose own chunk must still outlive it by the retention window
every chunk renamed, which forces a prune of more than 1,000 keys
every same-site asset URL in every live page must resolve, including the half-failed state and v1 pages still open in a tab
pass order, headers, charsets, and content-hashed names under
assets/refused deploys must not have made a single aws call
An
awsshim restores two S3 behaviors moto lacks: the 1,000-key limit ondelete-objects, and the empty quiet-mode response. It also returns invalidation status the way CloudFront does (InProgress, thenCompleted, orAccessDenied), logs calls for the order check, and injects the mid-deploy failure. Asleepshim logs how long the deploy waited, so the invalidation wait is checked exactly rather than by the clock.Verification
COMPLETED, a poll loop that stops sleeping or overshoots its wait budget, done marker before invalidation, a missing distribution-ID guard, a single delete batch over 1,000 keys, and a missing charset.DOCS_INVALIDATION_WAIT_SECONDS, default 15 minutes). If it hasn't landed by then, dropped static files are left for the next deploy to delete, with a warning.setup-node@v3warning.CI(main.yml) run is in progress or queued:gh run list -R keploy/docs -w main.yml -L 5. Runs created before this change aren't in the concurrency group, so they won't wait for the first new deploy. Theirdelete-removedwould undo it.cloudfront:GetInvalidationon the docs distribution. The deploy polls it before deleting static files the build dropped. Please make sure the deploy user has it before merging: without it, every deploy fails after the pages are live, naming the permission (deliberately loud, so removed files like a retracted PDF don't stay public indefinitely). Everything else is what the old action already used (S3 list/get/put/delete andCreateInvalidation). Manifests live underdocs/like everything else.Content-Encoding: gzipeven on clients that asked foridentity. The new deploy uploads files uncompressed. CloudFront's automatic compression is already on for these paths (verified live on/docs/pdf/*.pdfand/docs/assets/*.map, which the old action never gzipped), so browsers now get brotli where they support it and correct content negotiation.