mirror of https://github.com/pyodide/pyodide.git
Deploy py-compiled build (#3701)
Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
This commit is contained in:
parent
bce105bd69
commit
7f4f66b34b
|
@ -464,8 +464,7 @@ jobs:
|
||||||
# similar as possible to the steps in deploy-dev!
|
# similar as possible to the steps in deploy-dev!
|
||||||
resource_class: small
|
resource_class: small
|
||||||
|
|
||||||
docker:
|
<<: *defaults
|
||||||
- image: cibuilds/github:0.13
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
@ -474,8 +473,11 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Install requirements
|
name: Install requirements
|
||||||
command: |
|
command: |
|
||||||
apk add --no-cache --update python3 make npm
|
python3 -m pip install -e "./pyodide-build[deploy]"
|
||||||
python3 -m pip install awscli
|
|
||||||
|
wget https://github.com/tcnksm/ghr/releases/download/v0.16.0/ghr_v0.16.0_linux_amd64.tar.gz
|
||||||
|
tar xzf ghr_v0.16.0_linux_amd64.tar.gz
|
||||||
|
mv ghr_v0.16.0_linux_amd64/ghr /tmp/ghr-bin
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Deploy Github Releases
|
name: Deploy Github Releases
|
||||||
|
@ -487,7 +489,7 @@ jobs:
|
||||||
tar cjf dist/pyodide-${CIRCLE_TAG}.tar.bz2 pyodide/
|
tar cjf dist/pyodide-${CIRCLE_TAG}.tar.bz2 pyodide/
|
||||||
tar cjf dist/pyodide-core-${CIRCLE_TAG}.tar.bz2 pyodide/pyodide{.js,.mjs,.asm.js,.asm.wasm} pyodide/{package,repodata}.json pyodide/python_stdlib.zip
|
tar cjf dist/pyodide-core-${CIRCLE_TAG}.tar.bz2 pyodide/pyodide{.js,.mjs,.asm.js,.asm.wasm} pyodide/{package,repodata}.json pyodide/python_stdlib.zip
|
||||||
tar cjf dist/xbuildenv-${CIRCLE_TAG}.tar.bz2 xbuildenv/
|
tar cjf dist/xbuildenv-${CIRCLE_TAG}.tar.bz2 xbuildenv/
|
||||||
ghr -t "${GITHUB_TOKEN}" -u "${CIRCLE_PROJECT_USERNAME}" \
|
/tmp/ghr-bin -t "${GITHUB_TOKEN}" -u "${CIRCLE_PROJECT_USERNAME}" \
|
||||||
-r "${CIRCLE_PROJECT_REPONAME}" -c "${CIRCLE_SHA1}" \
|
-r "${CIRCLE_PROJECT_REPONAME}" -c "${CIRCLE_SHA1}" \
|
||||||
-delete "${CIRCLE_TAG}" \
|
-delete "${CIRCLE_TAG}" \
|
||||||
dist
|
dist
|
||||||
|
@ -500,39 +502,30 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Set PYODIDE_BASE_URL
|
name: Set PYODIDE_BASE_URL
|
||||||
command: |
|
command: |
|
||||||
|
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/v${CIRCLE_TAG}/pyc/" make dist/console.html
|
||||||
|
cp dist/console.html dist-pyc/console.html
|
||||||
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/v${CIRCLE_TAG}/debug/" make dist/console.html
|
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/v${CIRCLE_TAG}/debug/" make dist/console.html
|
||||||
cp dist/console.html dist-debug/console.html
|
cp dist/console.html dist-debug/console.html
|
||||||
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/v${CIRCLE_TAG}/full/" make dist/console.html
|
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/v${CIRCLE_TAG}/full/" make dist/console.html
|
||||||
- run:
|
- run:
|
||||||
name: Deploy to pyodide-cdn2.iodide.io
|
name: Deploy to S3
|
||||||
command: |
|
command: |
|
||||||
# Note: the following compression is the compression of files on S3
|
python3 tools/deploy_s3.py dist/ "v${CIRCLE_TAG}/full/" --bucket "pyodide-cdn2.iodide.io" --cache-control 'max-age=30758400, immutable, public'
|
||||||
# to reduce storage size and outbound AWS traffic. The HTTP
|
|
||||||
# requests for these files made to S3 will result in a response
|
|
||||||
# with gzip compression. However because JsDelivr CDN proxies and caches these
|
|
||||||
# requests, this has no impact on the compression of zip files and
|
|
||||||
# wheels once the files are served to users via CDN.
|
|
||||||
find dist/ -type f -print0 | xargs -0 -n1 -I@ bash -c "echo \"Compressing @\"; gzip @; mv @.gz @;"
|
|
||||||
aws s3 sync dist/ "s3://pyodide-cdn2.iodide.io/v${CIRCLE_TAG}/full/" --exclude '*.zip' --exclude '*.whl' --exclude "*.tar" --cache-control 'max-age=30758400, immutable, public' --content-encoding 'gzip' # 1 year cache
|
|
||||||
aws s3 sync dist/ "s3://pyodide-cdn2.iodide.io/v${CIRCLE_TAG}/full/" --exclude '*' --include '*.zip' --include "*.whl" --include "*.tar" --cache-control 'max-age=30758400, immutable, public' --content-type 'application/wasm' --content-encoding 'gzip' # 1 year
|
|
||||||
- run:
|
- run:
|
||||||
name: Deploy debug version to pyodide-cdn2.iodide.io
|
name: Deploy debug version to S3
|
||||||
command: |
|
command: |
|
||||||
find dist-debug/ -type f -print0 | xargs -0 -n1 -I@ bash -c "echo \"Compressing @\"; gzip @; mv @.gz @;"
|
python3 tools/deploy_s3.py dist-debug/ "v${CIRCLE_TAG}/debug/" --bucket "pyodide-cdn2.iodide.io" --cache-control 'max-age=30758400, immutable, public'
|
||||||
aws s3 sync dist-debug/ "s3://pyodide-cdn2.iodide.io/v${CIRCLE_TAG}/debug/" --exclude '*.zip' --exclude "*.whl" --exclude "*.tar" --cache-control 'max-age=30758400, public' --content-encoding 'gzip' # 1 year cache
|
|
||||||
aws s3 sync dist-debug/ "s3://pyodide-cdn2.iodide.io/v${CIRCLE_TAG}/debug/" --exclude '*' --include '*.zip' --include "*.whl" --include "*.tar" --cache-control 'max-age=30758400, public' --content-type 'application/wasm' --content-encoding 'gzip' # 1 year cache
|
|
||||||
- run:
|
- run:
|
||||||
name: update 301 redirect for the /latest/* route.
|
name: Deploy the pyc version to S3
|
||||||
command: |
|
command: |
|
||||||
aws s3api put-bucket-website --cli-input-json file://.circleci/s3-website-config.json
|
python3 tools/deploy_s3.py dist-pyc/ "v${CIRCLE_TAG}/pyc/" --bucket "pyodide-cdn2.iodide.io" --cache-control 'max-age=30758400, immutable, public'
|
||||||
|
|
||||||
deploy-dev:
|
deploy-dev:
|
||||||
# To reduce chance of deployment issues, try to keep the steps here as
|
# To reduce chance of deployment issues, try to keep the steps here as
|
||||||
# similar as possible to the steps in deploy-release!
|
# similar as possible to the steps in deploy-release!
|
||||||
resource_class: small
|
resource_class: small
|
||||||
|
|
||||||
docker:
|
<<: *defaults
|
||||||
- image: cibuilds/github:0.13
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
@ -541,28 +534,27 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Install requirements
|
name: Install requirements
|
||||||
command: |
|
command: |
|
||||||
apk add --no-cache --update python3 make
|
python3 -m pip install -e "./pyodide-build[deploy]"
|
||||||
python3 -m pip install awscli
|
|
||||||
- run:
|
- run:
|
||||||
name: Set PYODIDE_BASE_URL
|
name: Set PYODIDE_BASE_URL
|
||||||
command: |
|
command: |
|
||||||
|
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/dev/pyc/" make dist/console.html
|
||||||
|
cp dist/console.html dist-pyc/console.html
|
||||||
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/dev/debug/" make dist/console.html
|
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/dev/debug/" make dist/console.html
|
||||||
cp dist/console.html dist-debug/console.html
|
cp dist/console.html dist-debug/console.html
|
||||||
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/dev/full/" make dist/console.html
|
PYODIDE_BASE_URL="https://cdn.jsdelivr.net/pyodide/dev/full/" make dist/console.html
|
||||||
- run:
|
- run:
|
||||||
name: Deploy to pyodide-cdn2.iodide.io
|
name: Deploy to S3
|
||||||
command: |
|
command: |
|
||||||
find dist/ -type f -print0 | xargs -0 -n1 -I@ bash -c "echo \"Compressing @\"; gzip @; mv @.gz @;"
|
python3 tools/deploy_s3.py dist/ "dev/full/" --bucket "pyodide-cdn2.iodide.io" --cache-control 'max-age=3600, public' --rm-remote-prefix
|
||||||
aws s3 rm --recursive "s3://pyodide-cdn2.iodide.io/dev/full/"
|
|
||||||
aws s3 sync dist/ "s3://pyodide-cdn2.iodide.io/dev/full/" --exclude '*.zip' --exclude '*.whl' --exclude "*.tar" --cache-control 'max-age=3600, public' --content-encoding 'gzip' # 1 hour cache
|
|
||||||
aws s3 sync dist/ "s3://pyodide-cdn2.iodide.io/dev/full/" --exclude '*' --include '*.zip' --include "*.whl" --include "*.tar" --cache-control 'max-age=3600, public' --content-type 'application/wasm' --content-encoding 'gzip' # 1 hour cache
|
|
||||||
- run:
|
- run:
|
||||||
name: Deploy debug version to pyodide-cdn2.iodide.io
|
name: Deploy debug version to S3
|
||||||
command: |
|
command: |
|
||||||
find dist-debug/ -type f -print0 | xargs -0 -n1 -I@ bash -c "echo \"Compressing @\"; gzip @; mv @.gz @;"
|
python3 tools/deploy_s3.py dist-debug/ "dev/debug/" --bucket "pyodide-cdn2.iodide.io" --cache-control 'max-age=3600, public' --rm-remote-prefix
|
||||||
aws s3 rm --recursive "s3://pyodide-cdn2.iodide.io/dev/debug/"
|
- run:
|
||||||
aws s3 sync dist-debug/ "s3://pyodide-cdn2.iodide.io/dev/debug/" --exclude '*.zip' --exclude '*.whl' --exclude "*.tar" --cache-control 'max-age=3600, public' --content-encoding 'gzip' # 1 hour cache
|
name: Deploy pyc version to S3
|
||||||
aws s3 sync dist-debug/ "s3://pyodide-cdn2.iodide.io/dev/debug/" --exclude '*' --include '*.zip' --include "*.whl" --include "*.tar" --cache-control 'max-age=3600, public' --content-type 'application/wasm' --content-encoding 'gzip' # 1 hour cache
|
command: |
|
||||||
|
python3 tools/deploy_s3.py dist-pyc/ "dev/pyc/" --bucket "pyodide-cdn2.iodide.io" --cache-control 'max-age=3600, public' --rm-remote-prefix
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
@ -849,6 +841,7 @@ workflows:
|
||||||
- test-packages-firefox
|
- test-packages-firefox
|
||||||
- build-pyodide-debug
|
- build-pyodide-debug
|
||||||
- create-xbuild-env
|
- create-xbuild-env
|
||||||
|
- build-test-pyc-packages
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
|
@ -861,6 +854,7 @@ workflows:
|
||||||
- test-core-firefox
|
- test-core-firefox
|
||||||
- test-packages-firefox
|
- test-packages-firefox
|
||||||
- build-pyodide-debug
|
- build-pyodide-debug
|
||||||
|
- build-test-pyc-packages
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: main
|
only: main
|
||||||
|
|
|
@ -57,7 +57,7 @@ jobs:
|
||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
run: |
|
run: |
|
||||||
mkdir test-results
|
mkdir test-results
|
||||||
cd pyodide-build && python3 -m pip install -e ".[test]" && cd ..
|
cd pyodide-build && python3 -m pip install -e ".[test,deploy]" && cd ..
|
||||||
python3 -m pip install pytest-cov hypothesis pytz
|
python3 -m pip install pytest-cov hypothesis pytz
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
|
@ -67,7 +67,7 @@ jobs:
|
||||||
--verbose \
|
--verbose \
|
||||||
--runtime=host \
|
--runtime=host \
|
||||||
--cov=pyodide_build --cov=pyodide \
|
--cov=pyodide_build --cov=pyodide \
|
||||||
src pyodide-build packages/micropip/ packages/_tests
|
src pyodide-build packages/micropip/ packages/_tests tools/
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
|
@ -139,6 +139,12 @@ iterable`. (Python async _iterables_ that were not also iterators were already
|
||||||
_iterable_ and an async iterator.)
|
_iterable_ and an async iterator.)
|
||||||
{pr}`3708`
|
{pr}`3708`
|
||||||
|
|
||||||
|
- {{ Enhancement }} A py-compiled build which has smaller and faster-to-load
|
||||||
|
packages is now deployed under
|
||||||
|
`https://cdn.jsdelivr.net/pyodide/v0.23.0/pyc/` (also for future
|
||||||
|
versions). The exceptions obtained with this builds will not include code
|
||||||
|
snippets however. {pr}`3701`
|
||||||
|
|
||||||
- {{ Breaking }} Removed support for calling functions from the root of `pyodide` package
|
- {{ Breaking }} Removed support for calling functions from the root of `pyodide` package
|
||||||
directly. This has been deprecated since v0.21.0. Now all functions are only available
|
directly. This has been deprecated since v0.21.0. Now all functions are only available
|
||||||
under submodules.
|
under submodules.
|
||||||
|
|
|
@ -61,6 +61,9 @@ pyodide.cli =
|
||||||
test =
|
test =
|
||||||
pytest
|
pytest
|
||||||
pytest-pyodide==0.23.2
|
pytest-pyodide==0.23.2
|
||||||
|
deploy =
|
||||||
|
boto3
|
||||||
|
moto
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = .
|
where = .
|
||||||
|
|
|
@ -6,8 +6,6 @@
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pathspec # pip install pathspec
|
|
||||||
|
|
||||||
|
|
||||||
def hash_file(filename):
|
def hash_file(filename):
|
||||||
with open(filename, "rb") as f:
|
with open(filename, "rb") as f:
|
||||||
|
@ -23,6 +21,8 @@ def get_ignore_pattern(root: Path) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
import pathspec # pip install pathspec
|
||||||
|
|
||||||
root: Path = Path(__file__).parent.parent
|
root: Path = Path(__file__).parent.parent
|
||||||
targets: list[Path] = [
|
targets: list[Path] = [
|
||||||
root / "Makefile",
|
root / "Makefile",
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
import gzip
|
||||||
|
import io
|
||||||
|
import mimetypes
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import botocore
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
def check_s3_object_exists(s3_client, bucket: str, object_name: str):
|
||||||
|
try:
|
||||||
|
s3_client.head_object(Bucket=bucket, Key=object_name)
|
||||||
|
return True
|
||||||
|
except botocore.exceptions.ClientError as e:
|
||||||
|
if e.response["Error"]["Code"] == "404":
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_remote_prefix_to_remove(remote_prefix: Path) -> None:
|
||||||
|
"""Check remote prefix to remove
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> _validate_remote_prefix_to_remove(Path("dev/full/"))
|
||||||
|
>>> _validate_remote_prefix_to_remove(Path("dev/abc2/"))
|
||||||
|
>>> _validate_remote_prefix_to_remove(Path("/"))
|
||||||
|
Traceback (most recent call last):
|
||||||
|
ValueError: Remote prefix to remove should be at least 2 levels deep. For example, 'dev/full/'
|
||||||
|
>>> _validate_remote_prefix_to_remove(Path("v0.17.0/full/"))
|
||||||
|
Traceback (most recent call last):
|
||||||
|
ValueError: Remote prefix to remove should start with 'dev' (without leading '/'). For example, 'dev/full/'
|
||||||
|
"""
|
||||||
|
prefix_parts = remote_prefix.parts
|
||||||
|
if len(prefix_parts) < 2:
|
||||||
|
raise ValueError(
|
||||||
|
"Remote prefix to remove should be at least 2 levels deep. "
|
||||||
|
"For example, 'dev/full/'"
|
||||||
|
)
|
||||||
|
if prefix_parts[0] != "dev":
|
||||||
|
raise ValueError(
|
||||||
|
"Remote prefix to remove should start with 'dev' (without leading '/'). "
|
||||||
|
"For example, 'dev/full/'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rm_s3_prefix(bucket: str, prefix: str):
|
||||||
|
"""Remove all objects under a given prefix"""
|
||||||
|
s3 = boto3.resource("s3")
|
||||||
|
bucket = s3.Bucket(bucket)
|
||||||
|
for obj in bucket.objects.filter(Prefix=prefix):
|
||||||
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def deploy_to_s3_main(
|
||||||
|
local_folder: Path = typer.Argument(..., help="Path to the local folder"),
|
||||||
|
remote_prefix: Path = typer.Argument(..., help="Remote prefix"),
|
||||||
|
bucket: str = typer.Option(..., help="bucket name"),
|
||||||
|
cache_control: str = typer.Option(
|
||||||
|
"max-age=30758400, immutable, public", help="Cache control header to set"
|
||||||
|
),
|
||||||
|
pretend: bool = typer.Option(False, help="Don't actually upload anything"),
|
||||||
|
overwrite: bool = typer.Option(False, help="Overwrite existing files"),
|
||||||
|
rm_remote_prefix: bool = typer.Option(
|
||||||
|
False, help="Remove existing files under the remote prefix"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""Deploy a dist folder with Pyodide packages to AWS S3"""
|
||||||
|
s3_client = boto3.client("s3")
|
||||||
|
|
||||||
|
typer.echo(f"Deploying {local_folder} to s3://{bucket}/{remote_prefix}")
|
||||||
|
typer.echo("Options: ")
|
||||||
|
typer.echo(f" - {cache_control=}")
|
||||||
|
typer.echo(f" - {pretend=}")
|
||||||
|
typer.echo(" - content-encoding: gzip")
|
||||||
|
|
||||||
|
if rm_remote_prefix:
|
||||||
|
_validate_remote_prefix_to_remove(remote_prefix)
|
||||||
|
if not pretend:
|
||||||
|
_rm_s3_prefix(bucket, str(remote_prefix).lstrip("/"))
|
||||||
|
|
||||||
|
for file_path in local_folder.glob("**/*"):
|
||||||
|
if not file_path.is_file():
|
||||||
|
continue
|
||||||
|
remote_path = remote_prefix / file_path.relative_to(local_folder)
|
||||||
|
|
||||||
|
if not overwrite and check_s3_object_exists(
|
||||||
|
s3_client, bucket, str(remote_path).lstrip("/")
|
||||||
|
):
|
||||||
|
typer.echo(
|
||||||
|
f"Cannot upload {file_path} because it already exists on s3://{bucket}/{remote_path}"
|
||||||
|
)
|
||||||
|
typer.echo("Exiting (use --overwrite to overwrite existing files)")
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
|
with open(file_path, "rb") as fh_in:
|
||||||
|
# Use gzip compression for storage. This only impacts storage on
|
||||||
|
# AWS and transfer between S3 and the CDN. It has no impact on the
|
||||||
|
# compression received by the end user (since the CDN re-compresses
|
||||||
|
# files).
|
||||||
|
fh_compressed = io.BytesIO()
|
||||||
|
with gzip.GzipFile(fileobj=fh_compressed, mode="w"):
|
||||||
|
shutil.copyfileobj(fh_in, fh_compressed)
|
||||||
|
|
||||||
|
fh_compressed.seek(0)
|
||||||
|
|
||||||
|
content_type = None
|
||||||
|
if file_path.suffix in (".zip", ".whl", ".tar", ".a"):
|
||||||
|
content_type = "application/wasm"
|
||||||
|
elif file_path.suffix == ".ts":
|
||||||
|
# This will not be correctly detected by mimetypes.
|
||||||
|
# However, JsDelivr will currently not serve .ts file in the
|
||||||
|
# custom CDN configuration, so it does not really matter.
|
||||||
|
content_type = "text/x.typescript"
|
||||||
|
else:
|
||||||
|
content_type = mimetypes.guess_type(file_path)[0]
|
||||||
|
if content_type is None:
|
||||||
|
content_type = "binary/octet-stream"
|
||||||
|
|
||||||
|
extra_args = {
|
||||||
|
"CacheControl": cache_control,
|
||||||
|
"ContentEncoding": "gzip",
|
||||||
|
"ContentType": content_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not pretend:
|
||||||
|
s3_client.upload_fileobj(
|
||||||
|
fh_compressed,
|
||||||
|
Bucket=bucket,
|
||||||
|
Key=str(remote_path).lstrip("/"),
|
||||||
|
ExtraArgs=extra_args,
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
f"Uploaded {file_path} to s3://{bucket}/{remote_path} ({content_type=})"
|
||||||
|
)
|
||||||
|
if pretend:
|
||||||
|
msg = "Would have " + msg
|
||||||
|
|
||||||
|
typer.echo(msg)
|
||||||
|
if pretend:
|
||||||
|
typer.echo(
|
||||||
|
"No files were actually uploaded. Set to pretend=False to upload files."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
|
@ -0,0 +1,131 @@
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import pytest
|
||||||
|
from moto import mock_s3
|
||||||
|
|
||||||
|
sys.path.append(str(Path(__file__).parents[1]))
|
||||||
|
from deploy_s3 import check_s3_object_exists, deploy_to_s3_main
|
||||||
|
|
||||||
|
|
||||||
|
@mock_s3
|
||||||
|
def test_check_s3_object_exists():
|
||||||
|
bucket_name = "mybucket"
|
||||||
|
s3_client = boto3.client("s3", region_name="us-east-1")
|
||||||
|
s3_client.create_bucket(Bucket=bucket_name)
|
||||||
|
|
||||||
|
s3_client.put_object(Bucket=bucket_name, Key="/a/test.txt", Body="test")
|
||||||
|
|
||||||
|
assert check_s3_object_exists(s3_client, bucket_name, "/a/test.txt") is True
|
||||||
|
assert check_s3_object_exists(s3_client, bucket_name, "/a/test2.txt") is False
|
||||||
|
|
||||||
|
|
||||||
|
@mock_s3
|
||||||
|
def test_deploy_to_s3_overwrite(tmp_path, capsys):
|
||||||
|
(tmp_path / "a.whl").write_text("a")
|
||||||
|
(tmp_path / "b.tar").write_text("b")
|
||||||
|
(tmp_path / "c.zip").write_text("c")
|
||||||
|
|
||||||
|
bucket_name = "mybucket"
|
||||||
|
s3_client = boto3.client("s3", region_name="us-east-1")
|
||||||
|
s3_client.create_bucket(Bucket=bucket_name)
|
||||||
|
|
||||||
|
deploy_to_s3_main(
|
||||||
|
tmp_path,
|
||||||
|
remote_prefix=PurePosixPath("dev/full/"),
|
||||||
|
bucket=bucket_name,
|
||||||
|
cache_control="max-age=30758400",
|
||||||
|
pretend=False,
|
||||||
|
overwrite=False,
|
||||||
|
rm_remote_prefix=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_object_list():
|
||||||
|
response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix="dev/full/")
|
||||||
|
return set(obj["Key"] for obj in response["Contents"])
|
||||||
|
|
||||||
|
assert get_object_list() == {"dev/full/a.whl", "dev/full/b.tar", "dev/full/c.zip"}
|
||||||
|
|
||||||
|
# Writing a second time to the same prefix with overwrite=False should fail
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
deploy_to_s3_main(
|
||||||
|
tmp_path,
|
||||||
|
remote_prefix=PurePosixPath("dev/full/"),
|
||||||
|
bucket=bucket_name,
|
||||||
|
cache_control="max-age=30758400",
|
||||||
|
pretend=False,
|
||||||
|
overwrite=False,
|
||||||
|
rm_remote_prefix=False,
|
||||||
|
)
|
||||||
|
msg = "Cannot upload .* because it already exists"
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
# Check for error message in last two lines of output
|
||||||
|
assert re.search(msg, "\n".join(captured.out.splitlines()[-2:]))
|
||||||
|
|
||||||
|
# Setting overwrite=True should overwrite the files
|
||||||
|
deploy_to_s3_main(
|
||||||
|
tmp_path,
|
||||||
|
remote_prefix=PurePosixPath("dev/full/"),
|
||||||
|
bucket=bucket_name,
|
||||||
|
cache_control="max-age=30758400",
|
||||||
|
pretend=False,
|
||||||
|
overwrite=True,
|
||||||
|
rm_remote_prefix=False,
|
||||||
|
)
|
||||||
|
assert get_object_list() == {"dev/full/a.whl", "dev/full/b.tar", "dev/full/c.zip"}
|
||||||
|
|
||||||
|
# Setting rm_remote_prefix=True, should remove remote files that don't exist locally
|
||||||
|
(tmp_path / "b.tar").unlink()
|
||||||
|
|
||||||
|
deploy_to_s3_main(
|
||||||
|
tmp_path,
|
||||||
|
remote_prefix=PurePosixPath("dev/full/"),
|
||||||
|
bucket=bucket_name,
|
||||||
|
cache_control="max-age=30758400",
|
||||||
|
pretend=False,
|
||||||
|
overwrite=False,
|
||||||
|
rm_remote_prefix=True,
|
||||||
|
)
|
||||||
|
assert get_object_list() == {"dev/full/c.zip", "dev/full/a.whl"}
|
||||||
|
|
||||||
|
|
||||||
|
@mock_s3
|
||||||
|
def test_deploy_to_s3_mime_type(tmp_path, capsys):
|
||||||
|
for ext in ["whl", "tar", "zip", "js", "ts", "json", "ttf", "a", "mjs.map", "mjs"]:
|
||||||
|
(tmp_path / f"a.{ext}").write_text("a")
|
||||||
|
|
||||||
|
bucket_name = "mybucket"
|
||||||
|
s3_client = boto3.client("s3", region_name="us-east-1")
|
||||||
|
s3_client.create_bucket(Bucket=bucket_name)
|
||||||
|
|
||||||
|
deploy_to_s3_main(
|
||||||
|
tmp_path,
|
||||||
|
remote_prefix=PurePosixPath(""),
|
||||||
|
bucket=bucket_name,
|
||||||
|
cache_control="max-age=30758400",
|
||||||
|
pretend=False,
|
||||||
|
overwrite=False,
|
||||||
|
rm_remote_prefix=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_header(key, field="content-type"):
|
||||||
|
res = s3_client.get_object(Bucket=bucket_name, Key=key)
|
||||||
|
return res["ResponseMetadata"]["HTTPHeaders"][field]
|
||||||
|
|
||||||
|
assert get_header("a.js", "content-encoding") == "gzip"
|
||||||
|
|
||||||
|
# These MIME types we set explicitly for better CDN compression
|
||||||
|
assert get_header("a.whl") == "application/wasm"
|
||||||
|
assert get_header("a.tar") == "application/wasm"
|
||||||
|
assert get_header("a.zip") == "application/wasm"
|
||||||
|
assert get_header("a.a") == "application/wasm"
|
||||||
|
|
||||||
|
# The rest we set based on the file extension
|
||||||
|
assert get_header("a.js") == "text/javascript"
|
||||||
|
assert get_header("a.mjs") == "text/javascript"
|
||||||
|
assert get_header("a.ts") == "text/x.typescript"
|
||||||
|
assert get_header("a.json") == "application/json"
|
||||||
|
assert get_header("a.ttf") == "font/ttf"
|
||||||
|
assert get_header("a.mjs.map") == "binary/octet-stream"
|
Loading…
Reference in New Issue