diff --git a/workflow-compile.py b/.github/workflows/compile.py similarity index 95% rename from workflow-compile.py rename to .github/workflows/compile.py index 18e62f7..7835bb0 100644 --- a/workflow-compile.py +++ b/.github/workflows/compile.py @@ -5,10 +5,11 @@ from pathlib import Path +import plac + def main( file: "modified file from which to build artifacts", # type: ignore - libguides_groups: '{"groups":[{"slug":"foo","id":"999"},{…}]}', # type: ignore github_commit: ("optional github commit path", "option", "g"), # type: ignore ): print(f"🐞 file: {file}") @@ -62,14 +63,16 @@ def main( f.write(html) elif component == "header" or component == "footer": # NOTE libguides_groups is set in a GitHub Actions secret - slugs = [g["slug"] for g in json.loads(libguides_groups)["groups"]] + slugs = [g["slug"] for g in json.loads(os.environ.get("GROUPS"))["groups"]] slugs.append("system") variants = list(slugs) + print(f"🐞 variants: {variants}") variant = ( file.split(".")[0].split("-")[-1] if file.split(".")[0].split("-")[-1] in variants else None ) + print(f"🐞 variant: {variant}") # NOTE avoid redundant artifact creation if os.path.isfile(f'artifacts/{file.split("/")[-1]}') or os.path.isfile( f"artifacts/{component}--{variant}.html" @@ -83,6 +86,7 @@ def main( f.write(html) else: # NOTE header-wrapper.shtm triggers this condition, for example + print(f"🐞 variants: {variants}") for variant in variants: print(f"🐞 variant: {variant}") # reset output by copying html content into it @@ -140,5 +144,4 @@ def parse_nested_includes(wrapper, variant=None): if __name__ == "__main__": - # fmt: off - import plac; plac.call(main) + plac.call(main) diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml new file mode 100644 index 0000000..f6f47ca --- /dev/null +++ b/.github/workflows/compile.yml @@ -0,0 +1,37 @@ +name: Compile Artifacts + +on: + workflow_call: + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Set up Python ${{ vars.PYTHON_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: ${{ vars.PYTHON_VERSION }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install plac + curl --silent --show-error --location https://github.com/sass/dart-sass/releases/download/${{ vars.DART_SASS_VERSION }}/dart-sass-${{ vars.DART_SASS_VERSION }}-linux-x64.tar.gz | tar --extract --gzip + mv dart-sass/sass /usr/local/bin + - name: Create artifacts directory + run: mkdir -p artifacts + - name: Compile artifacts from changed files + env: + GROUPS: ${{ secrets.LIBGUIDES_GROUPS }} + run: for f in $(git diff-tree --no-commit-id --name-only -r ${{ github.sha }}); + do + python .github/workflows/compile.py "$f" --github-commit ${{ github.repository }}/commit/${{ github.sha }}; + done + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: compile-${{ github.sha }} + path: artifacts diff --git a/.github/workflows/deploy.py b/.github/workflows/deploy.py new file mode 100644 index 0000000..ca74899 --- /dev/null +++ b/.github/workflows/deploy.py @@ -0,0 +1,180 @@ +import json +import os +import sys + +from playwright.sync_api import Page, TimeoutError as PlaywrightTimeoutError + + +def test_deploy(page: Page): + for item in os.scandir("artifacts"): + try: + page.goto("/libapps/login.php") + page.fill("#s-libapps-email", os.environ.get("USERNAME")) + page.fill("#s-libapps-password", os.environ.get("PASSWORD")) + page.click("#s-libapps-login-button") + page.click("#s-lib-app-anchor") + page.click("#s-lib-app-menu a:text('LibGuides')") + if item.name.endswith(".css") or item.name.endswith(".js"): + page.click("#s-lg-admin-command-bar a:text('Admin')") + page.click("#s-lg-admin-command-bar a:text('Look & Feel')") + page.click("#s-lib-admin-tabs a:text('Custom JS/CSS')") + page.click("#s-lg-include-files_link") + page.set_input_files("#include_file", item.path) + elif item.name.endswith(".html"): + target = item.name.split("-")[0] + with open(item) as f: + html = f.read() + if target == "template": + page.click("#s-lg-admin-command-bar a:text('Admin')") + page.click("#s-lg-admin-command-bar a:text('Look & Feel')") + page.click("#s-lib-admin-tabs a:text('Page Layout')") + if item.name.split("-")[1] == "guide": + page.click("#s-lib-admin-tabs a:text('Guide')") + page.click("#s-lg-guide-templates_link") + page.click("#select2-chosen-2") + # NOTE template must already exist + # TODO account for template not found condition + page.fill("#s2id_autogen2_search", item.name.split(".")[0]) + page.press("#s2id_autogen2_search", "Enter") + # NOTE template takes time to load after select + page.wait_for_load_state("networkidle") + page.fill("#template_code", html) + page.click("#btn-save-template") + # NOTE must wait for success before moving on + page.wait_for_selector("#btn-save-template.btn-success") + if item.name.split("-")[1] == "search": + page.click("#s-lib-admin-tabs a:text('Search')") + page.click("#s-lg-tpl_link") + page.click("#select2-chosen-3") + # NOTE template must already exist + # TODO account for template not found condition + page.fill("#s2id_autogen3_search", item.name.split(".")[0]) + page.press("#s2id_autogen3_search", "Enter") + # NOTE template takes time to load after select + page.wait_for_load_state("networkidle") + page.fill("#template_code", html) + page.click("#btn-save-template") + # NOTE must wait for success before moving on + page.wait_for_selector("#btn-save-template.btn-success") + elif target == "widget": + page.click("#s-lg-admin-command-bar a:text('Content')") + page.click("#s-lg-admin-command-bar a:text('Assets')") + page.fill( + "#name", item.name.split("-", maxsplit=2)[-1].split(".")[0] + ) + page.click( + "#lg-admin-asset-filter .datatable-filter__button--submit" + ) + try: + page.wait_for_selector( + "#s-lg-admin-datatable-content_info:text('showing 1 to 1 of 1 entries')" + ) + page.click("#s-lg-admin-datatable-content a i.fa-edit") + except PlaywrightTimeoutError: + page.click("#s-lg-page-content button:text('Add Content Item')") + page.click("#s-lg-page-content a:text('Media / Widget')") + page.fill( + "#widget_name", + item.name.split("-", maxsplit=2)[-1].split(".")[0], + ) + page.fill("#embed_code", html) + page.click("#s-lib-alert-btn-first") + page.wait_for_selector( + "td:text('" + + item.name.split("-", maxsplit=2)[-1].split(".")[0] + + "')" + ) + elif target == "head": + variant = item.name.split(".")[0].split("-")[-1] + if variant == "system": + page.goto("/libguides/lookfeel.php?action=1") + page.fill("#jscss_code", html) + page.click("#s-lg-btn-save-jscss") + # NOTE must wait for success before moving on + page.wait_for_selector("#s-lg-btn-save-jscss.btn-success") + elif variant == "libanswers": + # NOTE JS/CSS files are uploaded in LibGuides + page.click("#s-lib-app-anchor") + page.click("#s-lib-app-menu a:text('LibAnswers')") + page.click("#s-la-cmd-bar-collapse a:text('Admin')") + page.click("#s-la-cmd-bar-collapse a:text('System Settings')") + page.click(".nav-tabs a:text('Look & Feel')") + page.fill("#instmetafield", html) + page.click("#instmetabut") + # NOTE must wait for success before moving on + page.wait_for_selector("#s-ui-notification :text('Success')") + elif variant == "libcal": + # NOTE JS/CSS files are uploaded in LibGuides + page.click("#s-lib-app-anchor") + page.click("#s-lib-app-menu a:text('LibCal')") + page.click("#s-lc-app-menu-adm a") # Admin + page.click("#s-lc-app-menu-adm a:text('Look & Feel')") + page.fill("#instmeta", html) + page.click("#instmeta ~ button") + else: + for group in json.loads(os.environ.get("GROUPS"))["groups"]: + if variant == group["slug"]: + page.goto( + f'/libguides/groups.php?action=3&group_id={group["id"]}' + ) + page.fill("#jscss_code", html) + page.click("#s-lg-btn-save-jscss") + # NOTE must wait for success before moving on + page.wait_for_selector( + "#s-lg-btn-save-jscss.btn-success" + ) + elif target == "header": + variant = item.name.split(".")[0].split("-")[-1] + if variant == "system": + page.goto("/libguides/lookfeel.php?action=0") + page.fill("#banner_html", html) + page.click("#banner_html + .btn-primary") + # TODO LibAnswers & LibCal + else: + for group in json.loads(os.environ.get("GROUPS"))["groups"]: + if variant == group["slug"]: + page.goto( + f'/libguides/groups.php?action=2&group_id={group["id"]}' + ) + page.fill("#banner_html", html) + page.click("#banner_html + .btn-primary") + # NOTE must wait for success before moving on + page.wait_for_selector("#banner_html + .btn-success") + elif target == "footer": + variant = item.name.split(".")[0].split("-")[-1] + if variant == "system": + page.goto("/libguides/lookfeel.php?action=0") + page.click("#s-lg-footer_link") + page.fill("#footer_code", html) + page.click("#s-lg-btn-save-footer") + # NOTE LibAnswers uses the same system footer + page.click("#s-lib-app-anchor") + page.click("#s-lib-app-menu a:text('LibAnswers')") + page.click("#s-la-cmd-bar-collapse a:text('Admin')") + page.click("#s-la-cmd-bar-collapse a:text('System Settings')") + page.click(".nav-tabs a:text('Look & Feel')") + page.fill("#instfooterfield", html) + page.click("#instfooterbut") + # NOTE LibCal uses the same system footer + page.click("#s-la-app-anchor") + page.click("#s-la-app-menu a:text('LibCal')") + page.click("#s-lc-app-menu-adm a") # Admin + page.click("#s-lc-app-menu-adm a:text('Look & Feel')") + page.fill("#instfooter", html) + page.click("#instfooter ~ button") + else: + for group in json.loads(os.environ.get("GROUPS"))["groups"]: + if variant == group["slug"]: + page.goto( + f'/libguides/groups.php?action=2&group_id={group["id"]}' + ) + page.click("#s-lg-footer_link") + page.fill("#footer_code", html) + page.click("#s-lg-btn-save-footer") + # NOTE must wait for success before moving on + page.wait_for_selector( + "#s-lg-btn-save-footer.btn-success" + ) + except PlaywrightTimeoutError: + page.close() + sys.exit(f"PLAYWRIGHT_TIMEOUT: {item.name}", end="") diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..db27c4b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,47 @@ +name: Deploy Components + +on: + push: + paths: + - "**.scss" + - "*.js" + - "*.html" + - "*.shtm" + +jobs: + compile: + uses: ./.github/workflows/compile.yml + secrets: inherit + deploy: + runs-on: ubuntu-latest + needs: compile + container: + image: mcr.microsoft.com/playwright/python:v${{ vars.PLAYWRIGHT_VERSION }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Set up Python ${{ vars.PYTHON_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: ${{ vars.PYTHON_VERSION }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install playwright==${{ vars.PLAYWRIGHT_VERSION }} pytest-playwright + - uses: actions/download-artifact@v3 + with: + name: compile-${{ github.sha }} + path: artifacts + - name: Deploy components + id: deploy + env: + USERNAME: ${{ secrets.ADMIN_USERNAME }} + PASSWORD: ${{ secrets.ADMIN_PASSWORD }} + GROUPS: ${{ secrets.LIBGUIDES_GROUPS }} + run: pytest --slowmo 100 --video on --output playwright --base-url ${{ secrets.ADMIN_BASE_URL }} -sv .github/workflows/deploy.py + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: deploy-${{ github.sha }} + path: playwright diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml deleted file mode 100644 index 6e04af7..0000000 --- a/.github/workflows/workflow.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Compile & Deploy to CMS -on: - push: - paths: - - "**.scss" - - "*.js" - - "*.html" - - "*.shtm" - workflow_dispatch: -jobs: - compile-and-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - playwright install firefox - curl --silent --location https://github.com/sass/dart-sass/releases/download/1.53.0/dart-sass-1.53.0-linux-x64.tar.gz | tar --extract --gzip - mv dart-sass/sass /usr/local/bin - - name: Create artifacts directory - run: mkdir -p artifacts - - name: Compile artifacts from changed files - # NOTE the LIBGUIDES_GROUPS secret must be single-quoted because it contains JSON - run: for f in $(git diff-tree --no-commit-id --name-only -r ${{ github.sha }}); - do - python workflow-compile.py "$f" '${{ secrets.LIBGUIDES_GROUPS }}' --github-commit ${{ github.repository }}/commit/${{ github.sha }}; - done - - name: Deploy artifacts - id: deploy - # NOTE the LIBGUIDES_GROUPS secret must be single-quoted because it contains JSON - run: | - echo "STATUS=$(python workflow-deploy.py ${{ secrets.ADMIN_BASE_URL }} ${{ secrets.ADMIN_USERNAME }} ${{ secrets.ADMIN_PASSWORD }} '${{ secrets.LIBGUIDES_GROUPS }}')" >> $GITHUB_OUTPUT - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: ${{ github.workflow }} Artifacts-${{ github.run_number }} - path: artifacts - - name: Check deploy output - if: ${{ steps.deploy.outputs.STATUS }} - run: | - echo ${{ steps.deploy.outputs.STATUS }} - exit 1 diff --git a/workflow-deploy.py b/workflow-deploy.py deleted file mode 100644 index ae68fc9..0000000 --- a/workflow-deploy.py +++ /dev/null @@ -1,197 +0,0 @@ -import json -import os -import sys - -from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError - - -def main( - admin_base_url: "base url for admin access", # type: ignore - admin_username: "username for admin access", # type: ignore - admin_password: "password for admin access", # type: ignore - libguides_groups: '{"groups":[{"slug":"foo","id":"999"},{…}]}', # type: ignore -): - for item in os.scandir("artifacts"): - with sync_playwright() as playwright: - try: - b = playwright.firefox.launch() - p = b.new_page(base_url=admin_base_url, record_video_dir="artifacts") - p.goto("/libapps/login.php") - p.fill("#s-libapps-email", admin_username) - p.fill("#s-libapps-password", admin_password) - p.click("#s-libapps-login-button") - p.click("#s-lib-app-anchor") - p.click("#s-lib-app-menu a:text('LibGuides')") - if item.name.endswith(".css") or item.name.endswith(".js"): - p.click("#s-lg-admin-command-bar a:text('Admin')") - p.click("#s-lg-admin-command-bar a:text('Look & Feel')") - p.click("#s-lib-admin-tabs a:text('Custom JS/CSS')") - p.click("#s-lg-include-files_link") - p.set_input_files("#include_file", item.path) - elif item.name.endswith(".html"): - target = item.name.split("-")[0] - with open(item) as f: - html = f.read() - if target == "template": - p.click("#s-lg-admin-command-bar a:text('Admin')") - p.click("#s-lg-admin-command-bar a:text('Look & Feel')") - p.click("#s-lib-admin-tabs a:text('Page Layout')") - if item.name.split("-")[1] == "guide": - p.click("#s-lib-admin-tabs a:text('Guide')") - p.click("#s-lg-guide-templates_link") - p.click("#select2-chosen-2") - # NOTE template must already exist - # TODO account for template not found condition - p.fill("#s2id_autogen2_search", item.name.split(".")[0]) - p.press("#s2id_autogen2_search", "Enter") - # NOTE template takes time to load after select - p.wait_for_load_state("networkidle") - p.fill("#template_code", html) - p.click("#btn-save-template") - # NOTE must wait for success before moving on - p.wait_for_selector("#btn-save-template.btn-success") - if item.name.split("-")[1] == "search": - p.click("#s-lib-admin-tabs a:text('Search')") - p.click("#s-lg-tpl_link") - p.click("#select2-chosen-3") - # NOTE template must already exist - # TODO account for template not found condition - p.fill("#s2id_autogen3_search", item.name.split(".")[0]) - p.press("#s2id_autogen3_search", "Enter") - # NOTE template takes time to load after select - p.wait_for_load_state("networkidle") - p.fill("#template_code", html) - p.click("#btn-save-template") - # NOTE must wait for success before moving on - p.wait_for_selector("#btn-save-template.btn-success") - elif target == "widget": - p.click("#s-lg-admin-command-bar a:text('Content')") - p.click("#s-lg-admin-command-bar a:text('Assets')") - p.fill( - "#name", item.name.split("-", maxsplit=2)[-1].split(".")[0] - ) - p.click( - "#lg-admin-asset-filter .datatable-filter__button--submit" - ) - try: - p.wait_for_selector( - "#s-lg-admin-datatable-content_info:text('showing 1 to 1 of 1 entries')" - ) - p.click("#s-lg-admin-datatable-content a i.fa-edit") - except PlaywrightTimeoutError: - p.click( - "#s-lg-page-content button:text('Add Content Item')" - ) - p.click("#s-lg-page-content a:text('Media / Widget')") - p.fill( - "#widget_name", - item.name.split("-", maxsplit=2)[-1].split(".")[0], - ) - p.fill("#embed_code", html) - p.click("#s-lib-alert-btn-first") - p.wait_for_selector( - "td:text('" - + item.name.split("-", maxsplit=2)[-1].split(".")[0] - + "')" - ) - elif target == "head": - variant = item.name.split(".")[0].split("-")[-1] - if variant == "system": - p.goto("/libguides/lookfeel.php?action=1") - p.fill("#jscss_code", html) - p.click("#s-lg-btn-save-jscss") - # NOTE must wait for success before moving on - p.wait_for_selector("#s-lg-btn-save-jscss.btn-success") - elif variant == "libanswers": - # NOTE JS/CSS files are uploaded in LibGuides - p.click("#s-lib-app-anchor") - p.click("#s-lib-app-menu a:text('LibAnswers')") - p.click("#s-la-cmd-bar-collapse a:text('Admin')") - p.click("#s-la-cmd-bar-collapse a:text('System Settings')") - p.click(".nav-tabs a:text('Look & Feel')") - p.fill("#instmetafield", html) - p.click("#instmetabut") - # NOTE must wait for success before moving on - p.wait_for_selector("#s-ui-notification :text('Success')") - elif variant == "libcal": - # NOTE JS/CSS files are uploaded in LibGuides - p.click("#s-lib-app-anchor") - p.click("#s-lib-app-menu a:text('LibCal')") - p.click("#s-lc-app-menu-adm a") # Admin - p.click("#s-lc-app-menu-adm a:text('Look & Feel')") - p.fill("#instmeta", html) - p.click("#instmeta ~ button") - else: - for group in json.loads(libguides_groups)["groups"]: - if variant == group["slug"]: - p.goto( - f'/libguides/groups.php?action=3&group_id={group["id"]}' - ) - p.fill("#jscss_code", html) - p.click("#s-lg-btn-save-jscss") - # NOTE must wait for success before moving on - p.wait_for_selector( - "#s-lg-btn-save-jscss.btn-success" - ) - elif target == "header": - variant = item.name.split(".")[0].split("-")[-1] - if variant == "system": - p.goto("/libguides/lookfeel.php?action=0") - p.fill("#banner_html", html) - p.click("#banner_html + .btn-primary") - # TODO LibAnswers & LibCal - else: - for group in json.loads(libguides_groups)["groups"]: - if variant == group["slug"]: - p.goto( - f'/libguides/groups.php?action=2&group_id={group["id"]}' - ) - p.fill("#banner_html", html) - p.click("#banner_html + .btn-primary") - # NOTE must wait for success before moving on - p.wait_for_selector("#banner_html + .btn-success") - elif target == "footer": - variant = item.name.split(".")[0].split("-")[-1] - if variant == "system": - p.goto("/libguides/lookfeel.php?action=0") - p.click("#s-lg-footer_link") - p.fill("#footer_code", html) - p.click("#s-lg-btn-save-footer") - # NOTE LibAnswers uses the same system footer - p.click("#s-lib-app-anchor") - p.click("#s-lib-app-menu a:text('LibAnswers')") - p.click("#s-la-cmd-bar-collapse a:text('Admin')") - p.click("#s-la-cmd-bar-collapse a:text('System Settings')") - p.click(".nav-tabs a:text('Look & Feel')") - p.fill("#instfooterfield", html) - p.click("#instfooterbut") - # NOTE LibCal uses the same system footer - p.click("#s-la-app-anchor") - p.click("#s-la-app-menu a:text('LibCal')") - p.click("#s-lc-app-menu-adm a") # Admin - p.click("#s-lc-app-menu-adm a:text('Look & Feel')") - p.fill("#instfooter", html) - p.click("#instfooter ~ button") - else: - for group in json.loads(libguides_groups)["groups"]: - if variant == group["slug"]: - p.goto( - f'/libguides/groups.php?action=2&group_id={group["id"]}' - ) - p.click("#s-lg-footer_link") - p.fill("#footer_code", html) - p.click("#s-lg-btn-save-footer") - # NOTE must wait for success before moving on - p.wait_for_selector( - "#s-lg-btn-save-footer.btn-success" - ) - b.close() - except PlaywrightTimeoutError as e: - b.close() - print(f"PLAYWRIGHT_TIMEOUT: {item.name}", end="") - sys.exit() - - -if __name__ == "__main__": - # fmt: off - import plac; plac.call(main)