From 517e77f4796d70cb31879831ec9c9a9ccc1b1257 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:29:28 -0500 Subject: [PATCH 01/56] add gh actions workflow for building rpi image --- .github/workflows/build_rpi.yml | 144 ++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .github/workflows/build_rpi.yml diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml new file mode 100644 index 0000000..85a1e64 --- /dev/null +++ b/.github/workflows/build_rpi.yml @@ -0,0 +1,144 @@ +name: Build Raspberry Pi Image + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 21 + cache: 'npm' + - name: Install modules + run: npm i + - name: Build app + run: npm run build + + - name: Prepare Customization Scripts + run: | + mkdir -p customizations/stage-custom + + # 00-run.sh: Main script to execute customizations + cat << 'EOF' > customizations/stage-custom/00-run.sh + #!/bin/bash + set -e + + # Run Wi-Fi configuration + /pi-gen/stage-custom/01-wifi-config + + # Set up HTTP server + /pi-gen/stage-custom/02-http-server + EOF + + chmod +x customizations/stage-custom/00-run.sh + + # 01-wifi-config: Configure Wi-Fi credentials + cat << 'EOF' > customizations/stage-custom/01-wifi-config + #!/bin/bash + set -e + mkdir -p "${ROOTFS_DIR}/etc/wpa_supplicant" + + cat << WIFI_EOF > "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" + country=US + ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev + update_config=1 + + network={ + ssid="Your_SSID" + psk="Your_Password" + } + WIFI_EOF + + chmod 600 "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" + EOF + + chmod +x customizations/stage-custom/01-wifi-config + + # 02-http-server: Set up systemd service for HTTP server + cat << 'EOF' > customizations/stage-custom/02-http-server + #!/bin/bash + set -e + + # Install Python3 using chroot + chroot "${ROOTFS_DIR}" bash << CHROOT_EOF + apt-get update + apt-get install -y python3 + CHROOT_EOF + + # Create HTTP server script + cat << SERVER_EOF > "${ROOTFS_DIR}/usr/local/bin/simple-http-server.py" + #!/usr/bin/env python3 + from http.server import BaseHTTPRequestHandler, HTTPServer + + class SimpleHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b'Hello from Raspberry Pi!') + + if __name__ == '__main__': + server = HTTPServer(('0.0.0.0', 8080), SimpleHandler) + print('Starting server at http://0.0.0.0:8080') + server.serve_forever() + SERVER_EOF + + chmod +x "${ROOTFS_DIR}/usr/local/bin/simple-http-server.py" + + # Create and enable the systemd service + cat << SERVICE_EOF > "${ROOTFS_DIR}/etc/systemd/system/simple-http-server.service" + [Unit] + Description=Simple HTTP Server + After=network.target + + [Service] + ExecStart=/usr/local/bin/simple-http-server.py + Restart=always + User=pi + + [Install] + WantedBy=multi-user.target + SERVICE_EOF + + chroot "${ROOTFS_DIR}" bash << CHROOT_EOF + systemctl enable simple-http-server.service + CHROOT_EOF + EOF + + chmod +x customizations/stage-custom/02-http-server + + # Create the pi-gen config file + cat << CONFIG_EOF > customizations/config + IMG_NAME='CustomPiImage' + ENABLE_SSH=1 + CONFIG_EOF + + # Set up Docker Buildx (needed for multi-arch builds) + - name: Set Up Docker Buildx + uses: docker/setup-buildx-action@v1 + + # Build the custom Raspberry Pi image using pi-gen + - name: Build Custom Raspberry Pi Image + uses: usimd/pi-gen-action@v1 + with: + image-name: 'CustomPiImage' + stage-list: 'stage0 stage1 stage2 stage-custom' + config: './customizations/config' + custom-stages: './customizations/stage-custom' + export-last-stage-only: true + + # Upload the final image artifact + - name: Upload Image Artifact + uses: actions/upload-artifact@v2 + with: + name: custom-raspberry-pi-image + path: deploy/CustomPiImage.img From 95dab19f3878b511d9b295d9a95c5ea95e084e98 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:31:11 -0500 Subject: [PATCH 02/56] upload-artifact@v4 --- .github/workflows/build_rpi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index 85a1e64..da6eeb5 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -138,7 +138,7 @@ jobs: # Upload the final image artifact - name: Upload Image Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: custom-raspberry-pi-image path: deploy/CustomPiImage.img From 3cf7a511ef6032111048ad07f8625cc3f7429baa Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:36:26 -0500 Subject: [PATCH 03/56] try again --- .github/workflows/build_rpi.yml | 43 ++++++++++++--------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index da6eeb5..ac32f95 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -1,16 +1,14 @@ -name: Build Raspberry Pi Image +name: Build Custom Raspberry Pi Image on: push: branches: - main - pull_request: - workflow_dispatch: - jobs: build: runs-on: ubuntu-latest + steps: - uses: actions/checkout@v3 - name: Install Node.js @@ -23,12 +21,15 @@ jobs: - name: Build app run: npm run build - - name: Prepare Customization Scripts + - name: Set Up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Create Customization Scripts run: | - mkdir -p customizations/stage-custom + mkdir -p stage-custom # 00-run.sh: Main script to execute customizations - cat << 'EOF' > customizations/stage-custom/00-run.sh + cat << 'EOF' > stage-custom/00-run.sh #!/bin/bash set -e @@ -38,11 +39,10 @@ jobs: # Set up HTTP server /pi-gen/stage-custom/02-http-server EOF - - chmod +x customizations/stage-custom/00-run.sh + chmod +x stage-custom/00-run.sh # 01-wifi-config: Configure Wi-Fi credentials - cat << 'EOF' > customizations/stage-custom/01-wifi-config + cat << 'EOF' > stage-custom/01-wifi-config #!/bin/bash set -e mkdir -p "${ROOTFS_DIR}/etc/wpa_supplicant" @@ -60,16 +60,15 @@ jobs: chmod 600 "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" EOF - - chmod +x customizations/stage-custom/01-wifi-config + chmod +x stage-custom/01-wifi-config # 02-http-server: Set up systemd service for HTTP server - cat << 'EOF' > customizations/stage-custom/02-http-server + cat << 'EOF' > stage-custom/02-http-server #!/bin/bash set -e # Install Python3 using chroot - chroot "${ROOTFS_DIR}" bash << CHROOT_EOF + on_chroot << CHROOT_EOF apt-get update apt-get install -y python3 CHROOT_EOF @@ -91,7 +90,6 @@ jobs: print('Starting server at http://0.0.0.0:8080') server.serve_forever() SERVER_EOF - chmod +x "${ROOTFS_DIR}/usr/local/bin/simple-http-server.py" # Create and enable the systemd service @@ -109,34 +107,25 @@ jobs: WantedBy=multi-user.target SERVICE_EOF - chroot "${ROOTFS_DIR}" bash << CHROOT_EOF + on_chroot << CHROOT_EOF systemctl enable simple-http-server.service CHROOT_EOF EOF - - chmod +x customizations/stage-custom/02-http-server + chmod +x stage-custom/02-http-server # Create the pi-gen config file - cat << CONFIG_EOF > customizations/config + cat << CONFIG_EOF > config IMG_NAME='CustomPiImage' ENABLE_SSH=1 CONFIG_EOF - # Set up Docker Buildx (needed for multi-arch builds) - - name: Set Up Docker Buildx - uses: docker/setup-buildx-action@v1 - - # Build the custom Raspberry Pi image using pi-gen - name: Build Custom Raspberry Pi Image uses: usimd/pi-gen-action@v1 with: image-name: 'CustomPiImage' stage-list: 'stage0 stage1 stage2 stage-custom' - config: './customizations/config' - custom-stages: './customizations/stage-custom' export-last-stage-only: true - # Upload the final image artifact - name: Upload Image Artifact uses: actions/upload-artifact@v4 with: From fbdd72f14ba70fd1abf59926214c3a90e24be37f Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 05:37:39 -0500 Subject: [PATCH 04/56] run in pr --- .github/workflows/build_rpi.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index ac32f95..143fc3c 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -1,9 +1,12 @@ -name: Build Custom Raspberry Pi Image +name: Build Raspberry Pi Image on: push: branches: - main + pull_request: + workflow_dispatch: + jobs: build: From 2b99412d369045699f20a4c77bf43ce04d412110 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 06:31:21 -0500 Subject: [PATCH 05/56] try from example https://github.com/RaspAP/raspap-webgui/blob/master/.github/workflows/release.yml --- .github/workflows/build_rpi.yml | 119 ++++---------------- stage-custom/package-setup/00-run-chroot.sh | 58 ++++++++++ stage-custom/prerun.sh | 4 + 3 files changed, 83 insertions(+), 98 deletions(-) create mode 100644 stage-custom/package-setup/00-run-chroot.sh create mode 100644 stage-custom/prerun.sh diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index 143fc3c..95c7ed6 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -11,6 +11,14 @@ on: jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + include: + - arch: "32-bit" + pi_gen_version: "master" + - arch: "64-bit" + pi_gen_version: "arm64" + fail-fast: false steps: - uses: actions/checkout@v3 @@ -27,107 +35,22 @@ jobs: - name: Set Up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Create Customization Scripts - run: | - mkdir -p stage-custom - - # 00-run.sh: Main script to execute customizations - cat << 'EOF' > stage-custom/00-run.sh - #!/bin/bash - set -e - - # Run Wi-Fi configuration - /pi-gen/stage-custom/01-wifi-config - - # Set up HTTP server - /pi-gen/stage-custom/02-http-server - EOF - chmod +x stage-custom/00-run.sh - - # 01-wifi-config: Configure Wi-Fi credentials - cat << 'EOF' > stage-custom/01-wifi-config - #!/bin/bash - set -e - mkdir -p "${ROOTFS_DIR}/etc/wpa_supplicant" - - cat << WIFI_EOF > "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" - country=US - ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev - update_config=1 - - network={ - ssid="Your_SSID" - psk="Your_Password" - } - WIFI_EOF - - chmod 600 "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" - EOF - chmod +x stage-custom/01-wifi-config - - # 02-http-server: Set up systemd service for HTTP server - cat << 'EOF' > stage-custom/02-http-server - #!/bin/bash - set -e - - # Install Python3 using chroot - on_chroot << CHROOT_EOF - apt-get update - apt-get install -y python3 - CHROOT_EOF - - # Create HTTP server script - cat << SERVER_EOF > "${ROOTFS_DIR}/usr/local/bin/simple-http-server.py" - #!/usr/bin/env python3 - from http.server import BaseHTTPRequestHandler, HTTPServer - - class SimpleHandler(BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.end_headers() - self.wfile.write(b'Hello from Raspberry Pi!') - - if __name__ == '__main__': - server = HTTPServer(('0.0.0.0', 8080), SimpleHandler) - print('Starting server at http://0.0.0.0:8080') - server.serve_forever() - SERVER_EOF - chmod +x "${ROOTFS_DIR}/usr/local/bin/simple-http-server.py" - - # Create and enable the systemd service - cat << SERVICE_EOF > "${ROOTFS_DIR}/etc/systemd/system/simple-http-server.service" - [Unit] - Description=Simple HTTP Server - After=network.target - - [Service] - ExecStart=/usr/local/bin/simple-http-server.py - Restart=always - User=pi - - [Install] - WantedBy=multi-user.target - SERVICE_EOF - - on_chroot << CHROOT_EOF - systemctl enable simple-http-server.service - CHROOT_EOF - EOF - chmod +x stage-custom/02-http-server - - # Create the pi-gen config file - cat << CONFIG_EOF > config - IMG_NAME='CustomPiImage' - ENABLE_SSH=1 - CONFIG_EOF - - name: Build Custom Raspberry Pi Image uses: usimd/pi-gen-action@v1 with: - image-name: 'CustomPiImage' - stage-list: 'stage0 stage1 stage2 stage-custom' - export-last-stage-only: true + # image-name: 'CustomPiImage' + # stage-list: 'stage0 stage1 stage2 stage-custom' + # config: './pi-gen-config/config' + # custom-stages: './pi-gen-config/stage-custom' + # export-last-stage-only: true + # increase-runner-disk-size: true + + image-name: "rpideploy-${{ github.ref_name }}-${{ matrix.arch }}" + enable-ssh: 1 + stage-list: stage0 stage1 stage2 ./stage-custom + verbose-output: true + pi-gen-version: ${{ matrix.pi_gen_version }} + # pi-gen-repository: RaspAP/pi-gen - name: Upload Image Artifact uses: actions/upload-artifact@v4 diff --git a/stage-custom/package-setup/00-run-chroot.sh b/stage-custom/package-setup/00-run-chroot.sh new file mode 100644 index 0000000..ab32bae --- /dev/null +++ b/stage-custom/package-setup/00-run-chroot.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e + +# Install required packages +apt-get update +apt-get install -y python3 + +# Configure Wi-Fi +mkdir -p /etc/wpa_supplicant + +cat << EOF > /etc/wpa_supplicant/wpa_supplicant.conf +country=US +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 + +network={ + ssid="Your_SSID" + psk="Your_Password" +} +EOF + +chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf + +# Set up a simple HTTP server +cat << SERVER_EOF > /usr/local/bin/simple-http-server.py +#!/usr/bin/env python3 +from http.server import BaseHTTPRequestHandler, HTTPServer + +class SimpleHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b'Hello from Raspberry Pi!') + +if __name__ == '__main__': + server = HTTPServer(('0.0.0.0', 8080), SimpleHandler) + print('Starting server at http://0.0.0.0:8080') + server.serve_forever() +SERVER_EOF +chmod +x /usr/local/bin/simple-http-server.py + +# Create and enable a systemd service for the HTTP server +cat << SERVICE_EOF > /etc/systemd/system/simple-http-server.service +[Unit] +Description=Simple HTTP Server +After=network.target + +[Service] +ExecStart=/usr/local/bin/simple-http-server.py +Restart=always +User=root + +[Install] +WantedBy=multi-user.target +SERVICE_EOF + +systemctl enable simple-http-server.service diff --git a/stage-custom/prerun.sh b/stage-custom/prerun.sh new file mode 100644 index 0000000..beb195d --- /dev/null +++ b/stage-custom/prerun.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +if [ ! -d "${ROOTFS_DIR}" ]; then + copy_previous +fi From 2a8d5183c6cda20d28887e29a8ccb30e2538747f Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 06:55:21 -0500 Subject: [PATCH 06/56] remove custom stage --- .github/workflows/build_rpi.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index 95c7ed6..c856294 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -14,8 +14,8 @@ jobs: strategy: matrix: include: - - arch: "32-bit" - pi_gen_version: "master" + # - arch: "32-bit" + # pi_gen_version: "master" - arch: "64-bit" pi_gen_version: "arm64" fail-fast: false @@ -47,7 +47,8 @@ jobs: image-name: "rpideploy-${{ github.ref_name }}-${{ matrix.arch }}" enable-ssh: 1 - stage-list: stage0 stage1 stage2 ./stage-custom + stage-list: stage0 stage1 stage2 + # stage-list: stage0 stage1 stage2 ./stage-custom verbose-output: true pi-gen-version: ${{ matrix.pi_gen_version }} # pi-gen-repository: RaspAP/pi-gen From 784a314ccdb31f59df15b7ef86e3f548cb722f80 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 06:58:51 -0500 Subject: [PATCH 07/56] mkdir /proc etc. --- stage-custom/package-setup/00-run-chroot.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stage-custom/package-setup/00-run-chroot.sh b/stage-custom/package-setup/00-run-chroot.sh index ab32bae..5b9f9c9 100644 --- a/stage-custom/package-setup/00-run-chroot.sh +++ b/stage-custom/package-setup/00-run-chroot.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +mkdir -p /proc /dev /sys /run /tmp + # Install required packages apt-get update apt-get install -y python3 From b22aff8489435277f1085227175ceedd2b4fb58f Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 07:03:19 -0500 Subject: [PATCH 08/56] re-add custom stage --- .github/workflows/build_rpi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index c856294..d977621 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -47,7 +47,7 @@ jobs: image-name: "rpideploy-${{ github.ref_name }}-${{ matrix.arch }}" enable-ssh: 1 - stage-list: stage0 stage1 stage2 + stage-list: stage0 stage1 stage2 ./stage-custom # stage-list: stage0 stage1 stage2 ./stage-custom verbose-output: true pi-gen-version: ${{ matrix.pi_gen_version }} From 6d00c401ff1e60eecc50becdbd0fdb53210fd744 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 08:47:58 -0500 Subject: [PATCH 09/56] copy+paste pre-existing workflow --- .github/workflows/build_rpi.yml | 83 +++++++++++++++++---------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index d977621..5ad2a7f 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -1,60 +1,65 @@ -name: Build Raspberry Pi Image +name: Build and publish RaspAP images -on: - push: - branches: - - main - pull_request: - workflow_dispatch: +permissions: + contents: write +on: + release: + types: [ published ] jobs: - build: + build-raspap-image: runs-on: ubuntu-latest strategy: matrix: include: - # - arch: "32-bit" - # pi_gen_version: "master" + - arch: "32-bit" + pi_gen_version: "master" - arch: "64-bit" pi_gen_version: "arm64" fail-fast: false - steps: - - uses: actions/checkout@v3 - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 21 - cache: 'npm' - - name: Install modules - run: npm i - - name: Build app - run: npm run build + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set Up Docker Buildx - uses: docker/setup-buildx-action@v1 + # - name: Add RaspAP Stage + # run: | + # mkdir -p stage-raspap/package-raspap && + # { + # cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF + # #!/bin/bash + # apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps + # curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 + # EOF + # } && + # chmod +x stage-raspap/package-raspap/00-run-chroot.sh && + # { + # cat > stage-raspap/prerun.sh <<-EOF + # #!/bin/bash -e + # if [ ! -d "\${ROOTFS_DIR}" ]; then + # copy_previous + # fi + # EOF + # } && + # chmod +x stage-raspap/prerun.sh - - name: Build Custom Raspberry Pi Image + - name: Build RaspAP Image + id: build uses: usimd/pi-gen-action@v1 with: - # image-name: 'CustomPiImage' - # stage-list: 'stage0 stage1 stage2 stage-custom' - # config: './pi-gen-config/config' - # custom-stages: './pi-gen-config/stage-custom' - # export-last-stage-only: true - # increase-runner-disk-size: true - - image-name: "rpideploy-${{ github.ref_name }}-${{ matrix.arch }}" + image-name: "raspap-${{ github.ref_name }}-${{ matrix.arch }}" enable-ssh: 1 - stage-list: stage0 stage1 stage2 ./stage-custom - # stage-list: stage0 stage1 stage2 ./stage-custom + stage-list: stage0 stage1 stage2 + # stage-list: stage0 stage1 stage2 ./stage-raspap verbose-output: true pi-gen-version: ${{ matrix.pi_gen_version }} - # pi-gen-repository: RaspAP/pi-gen + pi-gen-repository: RaspAP/pi-gen - - name: Upload Image Artifact - uses: actions/upload-artifact@v4 + - name: Upload Artifact + uses: svenstaro/upload-release-action@v2 with: - name: custom-raspberry-pi-image - path: deploy/CustomPiImage.img + asset_name: raspap-image-${{ github.ref_name }}-${{ matrix.arch }}.zip + file: ${{ steps.build.outputs.image-path }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref }} + overwrite: true From 781f722aa1573a2abccee91949a74fc2dfbe9db5 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:14:39 -0500 Subject: [PATCH 10/56] make workflow run in pr --- .github/workflows/build_rpi.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index 5ad2a7f..f8dc566 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -1,12 +1,16 @@ -name: Build and publish RaspAP images +name: Build Raspberry Pi Image + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + permissions: contents: write -on: - release: - types: [ published ] - jobs: build-raspap-image: runs-on: ubuntu-latest From d1cb76570b1f46f272f9e799773e2d91ada360ae Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:38:51 -0500 Subject: [PATCH 11/56] try increase-runner-disk-size --- .github/workflows/build_rpi.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index f8dc566..6bd9db2 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -58,6 +58,11 @@ jobs: verbose-output: true pi-gen-version: ${{ matrix.pi_gen_version }} pi-gen-repository: RaspAP/pi-gen + increase-runner-disk-size: true + export-last-stage-only: true + hostname: myhostname + username: myuser + password: mypassword - name: Upload Artifact uses: svenstaro/upload-release-action@v2 From 60c2642962d2b92fb46f88b5de94843fe4830f8d Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:12:01 -0500 Subject: [PATCH 12/56] copy existing workflow directly --- .github/workflows/build_rpi.yml | 111 +++++++++++++++----------------- 1 file changed, 52 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index 6bd9db2..ede5446 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -1,74 +1,67 @@ -name: Build Raspberry Pi Image - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - +name: Build and publish RaspAP images permissions: contents: write +on: + release: + types: [published] + jobs: build-raspap-image: runs-on: ubuntu-latest strategy: matrix: include: - - arch: "32-bit" - pi_gen_version: "master" - - arch: "64-bit" - pi_gen_version: "arm64" + - arch: "32-bit" + pi_gen_version: "master" + - arch: "64-bit" + pi_gen_version: "arm64" fail-fast: false steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Add RaspAP Stage + run: | + mkdir -p stage-raspap/package-raspap && + { + cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF + #!/bin/bash + apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps + curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 --check 0 - # - name: Add RaspAP Stage - # run: | - # mkdir -p stage-raspap/package-raspap && - # { - # cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF - # #!/bin/bash - # apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps - # curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 - # EOF - # } && - # chmod +x stage-raspap/package-raspap/00-run-chroot.sh && - # { - # cat > stage-raspap/prerun.sh <<-EOF - # #!/bin/bash -e - # if [ ! -d "\${ROOTFS_DIR}" ]; then - # copy_previous - # fi - # EOF - # } && - # chmod +x stage-raspap/prerun.sh + # Set Wi-Fi country to prevent RF kill + raspi-config nonint do_wifi_country "US" + EOF + } && + chmod +x stage-raspap/package-raspap/00-run-chroot.sh && + { + cat > stage-raspap/prerun.sh <<-EOF + #!/bin/bash -e + if [ ! -d "\${ROOTFS_DIR}" ]; then + copy_previous + fi + EOF + } && + chmod +x stage-raspap/prerun.sh - - name: Build RaspAP Image - id: build - uses: usimd/pi-gen-action@v1 - with: - image-name: "raspap-${{ github.ref_name }}-${{ matrix.arch }}" - enable-ssh: 1 - stage-list: stage0 stage1 stage2 - # stage-list: stage0 stage1 stage2 ./stage-raspap - verbose-output: true - pi-gen-version: ${{ matrix.pi_gen_version }} - pi-gen-repository: RaspAP/pi-gen - increase-runner-disk-size: true - export-last-stage-only: true - hostname: myhostname - username: myuser - password: mypassword + - name: Build RaspAP Image + id: build + uses: usimd/pi-gen-action@v1 + with: + image-name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}" + enable-ssh: 1 + stage-list: stage0 stage1 stage2 ./stage-raspap + verbose-output: true + pi-gen-version: ${{ matrix.pi_gen_version }} + pi-gen-repository: RaspAP/pi-gen - - name: Upload Artifact - uses: svenstaro/upload-release-action@v2 - with: - asset_name: raspap-image-${{ github.ref_name }}-${{ matrix.arch }}.zip - file: ${{ steps.build.outputs.image-path }} - repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} - overwrite: true + - name: Upload Artifact + uses: svenstaro/upload-release-action@v2 + with: + asset_name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}.img.zip" + file: ${{ steps.build.outputs.image-path }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.event.inputs.tag || github.ref }} + overwrite: true From 0448ab55275ef846de83638b7fa2670036a89abb Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:12:57 -0500 Subject: [PATCH 13/56] change to run on PRs --- .github/workflows/build_rpi.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index ede5446..7eabc29 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -4,8 +4,7 @@ permissions: contents: write on: - release: - types: [published] + pull_request: jobs: build-raspap-image: From a805730680397552b3b36fa190d284a9160950ee Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:17:42 -0500 Subject: [PATCH 14/56] use actions/upload-artifact --- .github/workflows/build_rpi.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index 7eabc29..c4ed13a 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -56,11 +56,17 @@ jobs: pi-gen-version: ${{ matrix.pi_gen_version }} pi-gen-repository: RaspAP/pi-gen - - name: Upload Artifact - uses: svenstaro/upload-release-action@v2 + # - name: Upload Artifact + # uses: svenstaro/upload-release-action@v2 + # with: + # asset_name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}.img.zip" + # file: ${{ steps.build.outputs.image-path }} + # repo_token: ${{ secrets.GITHUB_TOKEN }} + # tag: ${{ github.event.inputs.tag || github.ref }} + # overwrite: true + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 with: - asset_name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}.img.zip" - file: ${{ steps.build.outputs.image-path }} - repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.event.inputs.tag || github.ref }} - overwrite: true + name: rpi-deploy.img.zip + path: ${{ steps.build.outputs.image-path }} From 1868adfa41091e6a06e5538ce1362e41742cbe94 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:02:56 -0500 Subject: [PATCH 15/56] take workflow from jamtools/raspap-webgui --- .github/workflows/build_rpi.yml | 63 +++++++++++++++------------------ 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index c4ed13a..d230592 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -1,9 +1,12 @@ -name: Build and publish RaspAP images +name: Dispatched Build and publish RaspAP images permissions: contents: write on: + release: + types: [published] + workflow_dispatch: pull_request: jobs: @@ -21,29 +24,29 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Add RaspAP Stage - run: | - mkdir -p stage-raspap/package-raspap && - { - cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF - #!/bin/bash - apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps - curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 --check 0 + # - name: Add RaspAP Stage + # run: | + # mkdir -p stage-raspap/package-raspap && + # { + # cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF + # #!/bin/bash + # apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps + # curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 --check 0 - # Set Wi-Fi country to prevent RF kill - raspi-config nonint do_wifi_country "US" - EOF - } && - chmod +x stage-raspap/package-raspap/00-run-chroot.sh && - { - cat > stage-raspap/prerun.sh <<-EOF - #!/bin/bash -e - if [ ! -d "\${ROOTFS_DIR}" ]; then - copy_previous - fi - EOF - } && - chmod +x stage-raspap/prerun.sh + # # Set Wi-Fi country to prevent RF kill + # raspi-config nonint do_wifi_country "US" + # EOF + # } && + # chmod +x stage-raspap/package-raspap/00-run-chroot.sh && + # { + # cat > stage-raspap/prerun.sh <<-EOF + # #!/bin/bash -e + # if [ ! -d "\${ROOTFS_DIR}" ]; then + # copy_previous + # fi + # EOF + # } && + # chmod +x stage-raspap/prerun.sh - name: Build RaspAP Image id: build @@ -51,22 +54,14 @@ jobs: with: image-name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}" enable-ssh: 1 - stage-list: stage0 stage1 stage2 ./stage-raspap + stage-list: stage0 stage1 stage2 + # stage-list: stage0 stage1 stage2 ./stage-raspap verbose-output: true pi-gen-version: ${{ matrix.pi_gen_version }} pi-gen-repository: RaspAP/pi-gen - # - name: Upload Artifact - # uses: svenstaro/upload-release-action@v2 - # with: - # asset_name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}.img.zip" - # file: ${{ steps.build.outputs.image-path }} - # repo_token: ${{ secrets.GITHUB_TOKEN }} - # tag: ${{ github.event.inputs.tag || github.ref }} - # overwrite: true - - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: rpi-deploy.img.zip + name: rpi-deploy-${{ matrix.arch }}.img.zip path: ${{ steps.build.outputs.image-path }} From cea722a712568ab7fb626b71972d06c0c8e7af25 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:05:10 -0500 Subject: [PATCH 16/56] Create build_rpi.yml to test release workflow trigger --- .github/workflows/build_rpi.yml | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/build_rpi.yml diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml new file mode 100644 index 0000000..cd1f99a --- /dev/null +++ b/.github/workflows/build_rpi.yml @@ -0,0 +1,43 @@ +name: Dispatched Build and publish RaspAP images + +permissions: + contents: write + +on: + release: + types: [published] + workflow_dispatch: + pull_request: + +jobs: + build-raspap-image: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - arch: "32-bit" + pi_gen_version: "master" + - arch: "64-bit" + pi_gen_version: "arm64" + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build RaspAP Image + id: build + uses: usimd/pi-gen-action@v1 + with: + image-name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}" + enable-ssh: 1 + stage-list: stage0 stage1 stage2 + # stage-list: stage0 stage1 stage2 ./stage-raspap + verbose-output: true + pi-gen-version: ${{ matrix.pi_gen_version }} + pi-gen-repository: RaspAP/pi-gen + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: rpi-deploy-${{ matrix.arch }}.img.zip + path: ${{ steps.build.outputs.image-path }} From 16d78e71b7c0818c58234e27075b0e09d2430179 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:28:17 -0400 Subject: [PATCH 17/56] add chroot workflow --- .github/workflows/build_rpi_chroot.yml | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/build_rpi_chroot.yml diff --git a/.github/workflows/build_rpi_chroot.yml b/.github/workflows/build_rpi_chroot.yml new file mode 100644 index 0000000..c3f54ea --- /dev/null +++ b/.github/workflows/build_rpi_chroot.yml @@ -0,0 +1,30 @@ +name: Build Raspberry Pi image with chroot + +on: + push: + branches: + - "*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: pguyot/arm-runner-action@v2 + id: build_image + with: + commands: | + echo yay + + - name: Compress the release image + # if: github.ref == 'refs/heads/releng' || startsWith(github.ref, 'refs/tags/') + run: | + mv ${{ steps.build_image.outputs.image }} my-release-image.img + xz -0 -T 0 -v my-release-image.img + + - name: Upload release image + uses: actions/upload-artifact@v4 + # if: github.ref == 'refs/heads/releng' || startsWith(github.ref, 'refs/tags/') + with: + name: Release image + path: my-release-image.img.xz From 2520422bf3b540fd0c22a4c58ee129c663fb0370 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:15:15 -0400 Subject: [PATCH 18/56] introduce updatecli tool --- sampleapp/esbuild.ts | 19 + sampleapp/index.js | 2 + sampleapp/package.json | 20 + sampleapp/pnpm-lock.yaml | 935 ++++++++++++++++++ sampleapp/src/index.ts | 8 + updatecli/Dockerfile | 64 ++ updatecli/docker-compose.yml | 47 + .../new_stuff/updatecli.d/ansible/deploy.yml | 11 + .../roles/deploy_artifact/tasks/main.yml | 41 + .../deploy_artifact/templates/service.j2 | 10 + .../new_stuff/updatecli.d/github_release.yml | 22 + .../new_stuff/updatecli.d/local_file.yml | 18 + .../updatecli.d/scripts/check_esbuild.js | 21 + .../updatecli.d/scripts/compare_with_file.sh | 27 + .../updatecli.d/scripts/run_from_esbuild.sh | 4 + 15 files changed, 1249 insertions(+) create mode 100644 sampleapp/esbuild.ts create mode 100644 sampleapp/index.js create mode 100644 sampleapp/package.json create mode 100644 sampleapp/pnpm-lock.yaml create mode 100644 sampleapp/src/index.ts create mode 100644 updatecli/Dockerfile create mode 100644 updatecli/docker-compose.yml create mode 100644 updatecli/new_stuff/updatecli.d/ansible/deploy.yml create mode 100644 updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml create mode 100644 updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 create mode 100644 updatecli/new_stuff/updatecli.d/github_release.yml create mode 100644 updatecli/new_stuff/updatecli.d/local_file.yml create mode 100644 updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js create mode 100755 updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh create mode 100755 updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh diff --git a/sampleapp/esbuild.ts b/sampleapp/esbuild.ts new file mode 100644 index 0000000..dcf9f25 --- /dev/null +++ b/sampleapp/esbuild.ts @@ -0,0 +1,19 @@ +import {context as build} from 'esbuild'; + +setTimeout(async () => { + const ctx = await build({ + bundle: true, + platform: 'node', + entryPoints: ['./src/index.ts'], + outfile: 'dist/index.js', + }); + + await ctx.watch(); + + await ctx.serve({ + // host: 'jam.local', + port: 1380, + }); + + console.log('http://jam.local:1380'); +}); diff --git a/sampleapp/index.js b/sampleapp/index.js new file mode 100644 index 0000000..a071a6c --- /dev/null +++ b/sampleapp/index.js @@ -0,0 +1,2 @@ +"use strict"; +console.log("yo man dog son yeah hmm yeah so yeah"); diff --git a/sampleapp/package.json b/sampleapp/package.json new file mode 100644 index 0000000..ebbfd0a --- /dev/null +++ b/sampleapp/package.json @@ -0,0 +1,20 @@ +{ + "name": "sampleapp", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "esbuild": "^0.24.2" + }, + "dependencies": { + "@types/express": "^5.0.1", + "eventsource": "^3.0.6", + "express": "^5.1.0" + } +} diff --git a/sampleapp/pnpm-lock.yaml b/sampleapp/pnpm-lock.yaml new file mode 100644 index 0000000..3be39eb --- /dev/null +++ b/sampleapp/pnpm-lock.yaml @@ -0,0 +1,935 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/express': + specifier: ^5.0.1 + version: 5.0.1 + eventsource: + specifier: ^3.0.6 + version: 3.0.6 + express: + specifier: ^5.1.0 + version: 5.1.0 + devDependencies: + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + +packages: + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@5.0.1': + resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@22.14.1': + resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.1: + resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.6: + resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==} + engines: {node: '>=18.0.0'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.14.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.14.1 + + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 22.14.1 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@5.0.1': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.6 + '@types/serve-static': 1.15.7 + + '@types/http-errors@2.0.4': {} + + '@types/mime@1.3.5': {} + + '@types/node@22.14.1': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.9.18': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.14.1 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.14.1 + '@types/send': 0.17.4 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.1: {} + + eventsource@3.0.6: + dependencies: + eventsource-parser: 3.0.1 + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@8.2.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.1: {} + + toidentifier@1.0.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + wrappy@1.0.2: {} diff --git a/sampleapp/src/index.ts b/sampleapp/src/index.ts new file mode 100644 index 0000000..8f5d365 --- /dev/null +++ b/sampleapp/src/index.ts @@ -0,0 +1,8 @@ +import express from 'express'; +const app = express(); + +app.get('/', (req, res) => { + res.send('yeah but like yeah man yay dude lol\n'); +}); + +app.listen(3000); diff --git a/updatecli/Dockerfile b/updatecli/Dockerfile new file mode 100644 index 0000000..d6e456d --- /dev/null +++ b/updatecli/Dockerfile @@ -0,0 +1,64 @@ +# FROM ubuntu:22.04 +FROM python:3 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN rm -rf /var/lib/apt/lists/* +RUN apt-get update + +RUN apt-get install --allow-downgrades --fix-missing -y systemd systemd-sysv npm + +# FROM python:3 + +# ENV DEBIAN_FRONTEND=noninteractive + +# RUN apt-get update + +RUN mkdir -p /workspace/app +RUN mkdir -p /workspace/runners +RUN mkdir -p /workspace/data + +RUN curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_arm64.deb && \ + dpkg -i /tmp/updatecli_arm64.deb && \ + rm /tmp/updatecli_arm64.deb + +ARG NODE_VERSION=22 + +ENV PATH="$HOME/.fnm:$PATH" + +# RUN apt-get install -y unzip && \ +# curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir /usr/local/fnm && \ +# ln -s /usr/local/fnm/fnm /usr/local/bin/fnm && \ +# eval "$(fnm env)" && \ +# fnm install $NODE_VERSION && \ +# fnm default $NODE_VERSION && fnm use $NODE_VERSION + +# RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash +# ENV NVM_DIR=/root/.nvm +# RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} +# RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} +# RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} +# ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" +# RUN node --version +# RUN npm --version + +# RUN corepack enable pnpm + +RUN mkdir -p /app +WORKDIR /app + +RUN npm init -y +RUN npm i eventsource +COPY new_stuff/updatecli.d/scripts/check_esbuild.js . + +# # WORKDIR /workspace/app + +# # COPY ./workspace/repo_config.json /workspace/repo_config.json + +# # COPY ./dist-pkg/index-linux-x64 ./index + +RUN pip3 install ansible + +VOLUME [ "/sys/fs/cgroup" ] +STOPSIGNAL SIGRTMIN+3 +CMD ["/sbin/init"] diff --git a/updatecli/docker-compose.yml b/updatecli/docker-compose.yml new file mode 100644 index 0000000..7d0f6db --- /dev/null +++ b/updatecli/docker-compose.yml @@ -0,0 +1,47 @@ +services: + updatecli: + build: + context: . + # image: updatecli/updatecli:latest + # platform: linux/x86_64 + container_name: updatecli + # restart: unless-stopped + volumes: + # Map the local updatecli configuration into the container (read-only) + - ./new_stuff/updatecli.d:/etc/updatecli + # - ./new_stuff/updatecli.d/local_file.yml:/etc/updatecli/updatecli.yaml:ro + # - ./updatecli.yaml:/etc/updatecli/updatecli.yaml:ro + # Map the local app directory so updatecli can read and update your files + # - ./app:/app:rw + # Map /tmp from the host so temporary files are available + - ./tmp:/tmp:rw + + - /sys/fs/cgroup:/sys/fs/cgroup:rw # Let systemd mount cgroups + - tmpfs:/run # Mount tmpfs at /run + - tmpfs:/run/lock + # environment: + # # Provide your GitHub token if needed + # GITHUB_TOKEN: ${GITHUB_TOKEN} + privileged: true + cgroup: host + # command: /sbin/init + stdin_open: true + tty: true + + # command: "ls /etc/updatecli" + # command: "updatecli apply --config /etc/updatecli/local_file.yml" + # command: "apply --config /etc/updatecli/local_file.yml" + # command: "apply --config /etc/updatecli/updatecli.yaml" + + network_mode: "host" + # command: "manifest show --config /etc/updatecli/updatecli.yaml --debug" + # command: "--help" + # command: "apply --help" + # command: "apply --config /etc/updatecli/updatecli.yaml --debug" + +volumes: + tmpfs: + driver: local + driver_opts: + type: tmpfs + device: tmpfs diff --git a/updatecli/new_stuff/updatecli.d/ansible/deploy.yml b/updatecli/new_stuff/updatecli.d/ansible/deploy.yml new file mode 100644 index 0000000..e80972b --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/ansible/deploy.yml @@ -0,0 +1,11 @@ +- hosts: localhost + connection: local + become: true + vars: + service_name: myapp + service_description: "Local Service" + artifact_path: "/etc/updatecli/artifacts/{{ artifact_name }}" + service_user: root + service_group: root + roles: + - deploy_artifact diff --git a/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml b/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml new file mode 100644 index 0000000..37b4b1f --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml @@ -0,0 +1,41 @@ +- name: Install binary + copy: + src: "{{ artifact_path }}" + dest: "/etc/updatecli/{{ artifact_name }}" + mode: '0755' + +- name: Create systemd service file + template: + src: service.j2 + dest: "/etc/systemd/system/{{ service_name }}.service" + mode: '0644' + +- name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: true + + +- name: Enable and start the service + block: + + - name: Enable and restart the service + ansible.builtin.systemd: + name: "{{ service_name }}" + enabled: true + state: restarted + + rescue: + + - name: Fetch service logs after failure + shell: | + journalctl -u {{ service_name }}.service --no-pager -n 50 || echo "No logs found" + register: service_logs + ignore_errors: true + + - name: Display service logs + debug: + var: service_logs.stdout_lines + + - name: Fail with context + fail: + msg: "Service {{ service_name }} failed to start. See logs above." diff --git a/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 b/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 new file mode 100644 index 0000000..35c307f --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 @@ -0,0 +1,10 @@ +[Unit] +Description={{ service_description }} +After=network.target + +[Service] +ExecStart=/usr/bin/node /etc/updatecli/{{ artifact_name }} +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/updatecli/new_stuff/updatecli.d/github_release.yml b/updatecli/new_stuff/updatecli.d/github_release.yml new file mode 100644 index 0000000..1360cc1 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/github_release.yml @@ -0,0 +1,22 @@ +name: Pull latest GitHub release if changed +sources: + githubRelease: + kind: githubRelease + name: github version + spec: + owner: x + repository: y + token: "{{ requiredEnv `GITHUB_TOKEN` }}" +conditions: + alwaysTrue: + kind: shell + spec: + command: echo "true" +targets: + fetchBinary: + kind: shell + spec: + command: | + curl -L -o /opt/updatecli/artifacts/myapp https://github.com/x/y/releases/download/{{ source.githubRelease.version }}/myapp + chmod +x /opt/updatecli/artifacts/myapp + ansible-playbook /opt/updatecli/ansible/deploy.yml -e "artifact_name=myapp" diff --git a/updatecli/new_stuff/updatecli.d/local_file.yml b/updatecli/new_stuff/updatecli.d/local_file.yml new file mode 100644 index 0000000..140bf83 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/local_file.yml @@ -0,0 +1,18 @@ +name: Watch esbuild output file hash and deploy if changed +sources: + fileHash: + kind: shell + name: local hash + spec: + command: curl -s http://localhost:1380/index.js | sha256sum | cut -d' ' -f1 +conditions: + hashChanged: + kind: shell + sourceid: fileHash + spec: + command: /etc/updatecli/scripts/compare_with_file.sh /etc/updatecli/.last_index_hash +targets: + fetchBinary: + kind: shell + spec: + command: /etc/updatecli/scripts/run_from_esbuild.sh diff --git a/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js b/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js new file mode 100644 index 0000000..a3b3268 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js @@ -0,0 +1,21 @@ +const {EventSource} = require('eventsource'); +const {spawn} = require('node:child_process'); + +const origin = 'http://jam.local:1380'; + +setTimeout(async () => { + run(); + new EventSource(`${origin}/esbuild`).addEventListener('change', async e => { + run(); + }); +}); + +const run = () => { + spawn('updatecli', ['apply', '--config', '/etc/updatecli/local_file.yml'], { + env: { + PATH: process.env.PATH, + }, + cwd: process.cwd(), + stdio: 'inherit', + }); +}; diff --git a/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh b/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh new file mode 100755 index 0000000..6a4f317 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +COMPARE_FILE="$1" +SOURCE_VALUE="${2:-}" + +# If SOURCE_VALUE is empty, use last argument passed (from Updatecli) +if [ -z "$SOURCE_VALUE" ]; then + SOURCE_VALUE="${@: -1}" +fi + +if [ ! -f "$COMPARE_FILE" ]; then + echo "Compare file not found, creating with value: $SOURCE_VALUE" + echo "$SOURCE_VALUE" > "$COMPARE_FILE" + exit 0 +fi + +EXISTING_VALUE=$(cat "$COMPARE_FILE") + +if [ "$EXISTING_VALUE" = "$SOURCE_VALUE" ]; then + echo "No change needed. Value matches $COMPARE_FILE." + exit 0 +else + echo "Value changed. Updating $COMPARE_FILE." + echo "$SOURCE_VALUE" > "$COMPARE_FILE" + exit 0 +fi diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh b/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh new file mode 100755 index 0000000..bf41784 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh @@ -0,0 +1,4 @@ +curl -sL -o /etc/updatecli/artifacts/index.js http://localhost:1380/index.js +# chmod +x /etc/updatecli/artifacts/index.js +python3 -m ansible playbook /etc/updatecli/ansible/deploy.yml -e "artifact_name=index.js" +# ansible playbook /etc/updatecli/ansible/deploy.yml -e "artifact_name=index.js" From 15320a7eabd2d2e6fc178f7b567ce8d2dd58f98c Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:56:25 -0400 Subject: [PATCH 19/56] support watching esbuild and GH releases through systemd --- .../new_stuff/updatecli.d/local_file.yml | 6 +- .../updatecli.d/scripts/check_esbuild.js | 5 +- .../scripts/common/create_and_run_service.sh | 45 ++++++++ .../updatecli.d/scripts/compare_with_file.sh | 2 +- .../updatecli.d/scripts/run_from_esbuild.sh | 11 +- .../scripts/run_from_github_release.sh | 8 ++ .../services/check_esbuild.service | 10 ++ .../services/check_github_releases.service | 10 ++ .../services/check_github_releases.timer | 9 ++ .../updatecli.d/updatecli_github_commit.yml | 23 ++++ updatecli/pi-init.sh | 108 ++++++++++++++++++ 11 files changed, 227 insertions(+), 10 deletions(-) create mode 100755 updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh create mode 100755 updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh create mode 100644 updatecli/new_stuff/updatecli.d/services/check_esbuild.service create mode 100644 updatecli/new_stuff/updatecli.d/services/check_github_releases.service create mode 100644 updatecli/new_stuff/updatecli.d/services/check_github_releases.timer create mode 100644 updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml create mode 100644 updatecli/pi-init.sh diff --git a/updatecli/new_stuff/updatecli.d/local_file.yml b/updatecli/new_stuff/updatecli.d/local_file.yml index 140bf83..d964ea9 100644 --- a/updatecli/new_stuff/updatecli.d/local_file.yml +++ b/updatecli/new_stuff/updatecli.d/local_file.yml @@ -4,15 +4,15 @@ sources: kind: shell name: local hash spec: - command: curl -s http://localhost:1380/index.js | sha256sum | cut -d' ' -f1 + command: curl -s http://jam.local:1380/index.js | sha256sum | cut -d' ' -f1 conditions: hashChanged: kind: shell sourceid: fileHash spec: - command: /etc/updatecli/scripts/compare_with_file.sh /etc/updatecli/.last_index_hash + command: /home/jamtools/code/scripts/compare_with_file.sh ./.last_index_hash targets: fetchBinary: kind: shell spec: - command: /etc/updatecli/scripts/run_from_esbuild.sh + command: /home/jamtools/code/scripts/run_from_esbuild.sh myapp diff --git a/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js b/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js index a3b3268..3e682a6 100644 --- a/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js +++ b/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js @@ -5,13 +5,14 @@ const origin = 'http://jam.local:1380'; setTimeout(async () => { run(); - new EventSource(`${origin}/esbuild`).addEventListener('change', async e => { + new EventSource(origin + '/esbuild').addEventListener('change', async e => { run(); }); }); const run = () => { - spawn('updatecli', ['apply', '--config', '/etc/updatecli/local_file.yml'], { + spawn('/home/jamtools/code/scripts/run_from_esbuild.sh', ['myapp'], { + // spawn('updatecli', ['apply', '--config', '/home/jamtools/code/local_file.yml'], { env: { PATH: process.env.PATH, }, diff --git a/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh b/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh new file mode 100755 index 0000000..8f0a3a8 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -e + +SECONDS=0 + +SERVICE_NAME="$1" +COMMAND="$2" + +SYSTEMD_PATH="/etc/systemd/system/${SERVICE_NAME}.service" + +NEW_SERVICE_CONTENT="[Unit] +Description=$SERVICE_NAME +After=network.target + +[Service] +Type=simple +ExecStart=$COMMAND +Restart=on-failure + +[Install] +WantedBy=multi-user.target" + +echo "Before check took $SECONDS seconds" +if [ ! -f "$SYSTEMD_PATH" ] || ! echo "$NEW_SERVICE_CONTENT" | sudo diff - "$SYSTEMD_PATH" >/dev/null 2>&1; then + echo "Service file needs updating..." + echo "$NEW_SERVICE_CONTENT" | sudo tee "$SYSTEMD_PATH" > /dev/null + echo "Before reload took $SECONDS seconds" + sudo systemctl daemon-reload + echo "After reload took $SECONDS seconds" +else + echo "Service file is up to date" +fi + +echo "Before restart took $SECONDS seconds" + +echo "Restarting service..." +sudo systemctl enable "$SERVICE_NAME" + +echo "Before restart took $SECONDS seconds" +sudo systemctl restart "$SERVICE_NAME" + +echo "After restart took $SECONDS seconds" + +echo "Done!" diff --git a/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh b/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh index 6a4f317..7acdb82 100755 --- a/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh +++ b/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -euo pipefail +# set -euo pipefail COMPARE_FILE="$1" SOURCE_VALUE="${2:-}" diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh b/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh index bf41784..9551351 100755 --- a/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh +++ b/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh @@ -1,4 +1,7 @@ -curl -sL -o /etc/updatecli/artifacts/index.js http://localhost:1380/index.js -# chmod +x /etc/updatecli/artifacts/index.js -python3 -m ansible playbook /etc/updatecli/ansible/deploy.yml -e "artifact_name=index.js" -# ansible playbook /etc/updatecli/ansible/deploy.yml -e "artifact_name=index.js" +#!/usr/bin/env bash +set -e + +mkdir -p artifacts +curl -sL -o /home/jamtools/code/artifacts/index.js http://jam.local:1380/index.js + +/home/jamtools/code/scripts/common/create_and_run_service.sh $1 "node /home/jamtools/code/artifacts/index.js" diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh new file mode 100755 index 0000000..6a56517 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +mkdir -p artifacts +curl -sL -o artifacts/index.js "https://github.com/$1/$2/releases/download/$3/index.js" + +/home/jamtools/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/jamtools/code/artifacts/index.js" diff --git a/updatecli/new_stuff/updatecli.d/services/check_esbuild.service b/updatecli/new_stuff/updatecli.d/services/check_esbuild.service new file mode 100644 index 0000000..47749a2 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/services/check_esbuild.service @@ -0,0 +1,10 @@ +[Unit] +Description=Check esbuild output file hash and deploy if changed +After=network.target + +[Service] +Restart=always +ExecStart=/usr/bin/node /home/jamtools/code/scripts/check_esbuild.js + +[Install] +WantedBy=multi-user.target diff --git a/updatecli/new_stuff/updatecli.d/services/check_github_releases.service b/updatecli/new_stuff/updatecli.d/services/check_github_releases.service new file mode 100644 index 0000000..f1e7e97 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/services/check_github_releases.service @@ -0,0 +1,10 @@ +[Unit] +Description=Check GitHub releases +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/updatecli apply --config /home/jamtools/code/updatecli_github_commit.yml + +[Install] +WantedBy=multi-user.target diff --git a/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer b/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer new file mode 100644 index 0000000..9c3e846 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Timer for checking GitHub releases + +[Timer] +OnBootSec=1min +OnUnitActiveSec=1min + +[Install] +WantedBy=timers.target diff --git a/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml new file mode 100644 index 0000000..8fd42c7 --- /dev/null +++ b/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml @@ -0,0 +1,23 @@ +name: Pull latest commit on GitHub +sources: + newRelease: + kind: githubrelease + # name: github version + spec: + owner: jamtools + repository: github-releases-test + token: "{{ requiredEnv `GITHUB_TOKEN` }}" + +conditions: + checkIfCommitIsNew: + kind: shell + sourceid: newRelease + spec: + command: /home/jamtools/code/scripts/compare_with_file.sh /home/jamtools/code/.last_release_tag + +targets: + runFromRelease: + kind: shell + sourceid: newRelease + spec: + command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools github-releases-test diff --git a/updatecli/pi-init.sh b/updatecli/pi-init.sh new file mode 100644 index 0000000..bd2cdda --- /dev/null +++ b/updatecli/pi-init.sh @@ -0,0 +1,108 @@ + +mkdir code && cd code +sudo apt update +sudo apt install -y cockpit + +# you can then log into cockpit via web browser at https://jamscribe.local:9090 +# you use your actual linux user's creds to log in + +curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_arm64.deb && \ + sudo dpkg -i /tmp/updatecli_arm64.deb && \ + rm /tmp/updatecli_arm64.deb + +sudo apt install -y npm +# this command took a loooooong time to run +# let's see if we can avoid installing this way + +# also it took a while for the pi to initialize at first +# after a while, the ssh command worked + + +mkdir -p scripts + +echo " +const {EventSource} = require('eventsource'); +const {spawn} = require('node:child_process'); + +const origin = 'http://jam.local:1380'; + +setTimeout(async () => { + run(); + new EventSource('http://jam.local:1380/esbuild').addEventListener('change', async e => { + run(); + }); +}); + +const run = () => { + spawn('updatecli', ['apply', '--config', '/home/jamtools/code/local_file.yml'], { + env: { + PATH: process.env.PATH, + }, + cwd: process.cwd(), + stdio: 'inherit', + }); +}; +" > scripts/check_esbuild.js + +echo ' +#!/bin/bash +# set -e + +COMPARE_FILE="$1" +SOURCE_VALUE="${2:-}" + +# If SOURCE_VALUE is empty, use last argument passed (from Updatecli) +if [ -z "$SOURCE_VALUE" ]; then + SOURCE_VALUE="${@: -1}" +fi + +if [ ! -f "$COMPARE_FILE" ]; then + echo "Compare file not found, creating with value: $SOURCE_VALUE" + echo "$SOURCE_VALUE" > "$COMPARE_FILE" + exit 0 +fi + +EXISTING_VALUE=$(cat "$COMPARE_FILE") + +if [ "$EXISTING_VALUE" = "$SOURCE_VALUE" ]; then + echo "No change needed. Value matches $COMPARE_FILE." + exit 0 +else + echo "Value changed. Updating $COMPARE_FILE." + echo "$SOURCE_VALUE" > "$COMPARE_FILE" + exit 0 +fi +' > scripts/compare_with_file.sh + +chmod +x scripts/compare_with_file.sh + +echo ' +' > scripts/run_from_esbuild.sh + +chmod +x scripts/run_from_esbuild.sh + +echo " +name: Watch esbuild output file hash and deploy if changed +sources: + fileHash: + kind: shell + name: local hash + spec: + command: curl -s http://jam.local:1380/index.js | sha256sum | cut -d' ' -f1 +conditions: + hashChanged: + kind: shell + sourceid: fileHash + spec: + command: ./scripts/compare_with_file.sh ./.last_index_hash +targets: + fetchBinary: + kind: shell + spec: + command: ./scripts/run_from_esbuild.sh +" > local_file.yml + +npm init -y +npm i eventsource + +node scripts/check_esbuild.js From 5bd390d6f7ea84d6dd410ceedfdada9449fc47e6 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:52:07 -0400 Subject: [PATCH 20/56] more solidified gh release watcher --- sampleapp/src/index.ts | 6 ++++-- .../updatecli.d/scripts/common/create_and_run_service.sh | 1 + .../new_stuff/updatecli.d/scripts/compare_with_file.sh | 2 +- .../updatecli.d/scripts/run_from_github_release.sh | 9 ++++++--- .../updatecli.d/services/check_github_releases.service | 1 + .../updatecli.d/services/check_github_releases.timer | 2 +- .../new_stuff/updatecli.d/updatecli_github_commit.yml | 9 ++++++--- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/sampleapp/src/index.ts b/sampleapp/src/index.ts index 8f5d365..18c0205 100644 --- a/sampleapp/src/index.ts +++ b/sampleapp/src/index.ts @@ -2,7 +2,9 @@ import express from 'express'; const app = express(); app.get('/', (req, res) => { - res.send('yeah but like yeah man yay dude lol\n'); + res.send('this is working now yay! so excited lets go\n'); }); -app.listen(3000); +app.listen(3000, '0.0.0.0', () => { + console.log('Server started on port 3000'); +}); diff --git a/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh b/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh index 8f0a3a8..ce91ff0 100755 --- a/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh +++ b/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh @@ -17,6 +17,7 @@ After=network.target Type=simple ExecStart=$COMMAND Restart=on-failure +WorkingDirectory=/home/jamtools/code/artifacts [Install] WantedBy=multi-user.target" diff --git a/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh b/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh index 7acdb82..8c636af 100755 --- a/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh +++ b/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh @@ -19,7 +19,7 @@ EXISTING_VALUE=$(cat "$COMPARE_FILE") if [ "$EXISTING_VALUE" = "$SOURCE_VALUE" ]; then echo "No change needed. Value matches $COMPARE_FILE." - exit 0 + exit 1 else echo "Value changed. Updating $COMPARE_FILE." echo "$SOURCE_VALUE" > "$COMPARE_FILE" diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh index 6a56517..cb18e78 100755 --- a/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh +++ b/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh @@ -2,7 +2,10 @@ set -e -mkdir -p artifacts -curl -sL -o artifacts/index.js "https://github.com/$1/$2/releases/download/$3/index.js" +mkdir -p /home/jamtools/code/artifacts +curl -sL -o /home/jamtools/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" -/home/jamtools/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/jamtools/code/artifacts/index.js" +mkdir -p /home/jamtools/code/artifacts/dist +unzip -o /home/jamtools/code/artifacts/dist.zip -d /home/jamtools/code/artifacts/dist + +/home/jamtools/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/jamtools/code/artifacts/dist/server/dist/local-server.cjs" diff --git a/updatecli/new_stuff/updatecli.d/services/check_github_releases.service b/updatecli/new_stuff/updatecli.d/services/check_github_releases.service index f1e7e97..50126b7 100644 --- a/updatecli/new_stuff/updatecli.d/services/check_github_releases.service +++ b/updatecli/new_stuff/updatecli.d/services/check_github_releases.service @@ -5,6 +5,7 @@ After=network.target [Service] Type=oneshot ExecStart=/usr/bin/updatecli apply --config /home/jamtools/code/updatecli_github_commit.yml +EnvironmentFile=/home/jamtools/code/secrets.env [Install] WantedBy=multi-user.target diff --git a/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer b/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer index 9c3e846..5b6312b 100644 --- a/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer +++ b/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer @@ -3,7 +3,7 @@ Description=Timer for checking GitHub releases [Timer] OnBootSec=1min -OnUnitActiveSec=1min +OnUnitActiveSec=1hour [Install] WantedBy=timers.target diff --git a/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml index 8fd42c7..8ef50af 100644 --- a/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml +++ b/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml @@ -1,12 +1,15 @@ -name: Pull latest commit on GitHub +name: Pull latest release on GitHub sources: newRelease: kind: githubrelease # name: github version spec: owner: jamtools - repository: github-releases-test + repository: songdrive-releases token: "{{ requiredEnv `GITHUB_TOKEN` }}" + versionfilter: + kind: regex + pattern: "vjamscribe-.*$" conditions: checkIfCommitIsNew: @@ -20,4 +23,4 @@ targets: kind: shell sourceid: newRelease spec: - command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools github-releases-test + command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools songdrive-releases From 0b1cb3f5cc8cf361c6732c2080217e422a575dae Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Sat, 19 Apr 2025 12:22:13 -0400 Subject: [PATCH 21/56] several updates to clean up unstaged changes --- docker/Dockerfile | 18 +- docker/docker-compose.with_volumes.yml | 10 + docker/docker-compose.yml | 11 +- package-lock.json | 1341 ++++++++++++++++- package.json | 3 + .../testdata/fc-release-with-asset.json | 154 ++ .../testdata/first_release.json | 2 - test/test.ts | 43 +- test/testdata.ts | 15 + tsconfig.json | 1 + updatecli/Dockerfile | 4 +- .../new_stuff/updatecli.d/ansible/deploy.yml | 11 - .../roles/deploy_artifact/tasks/main.yml | 41 - .../deploy_artifact/templates/service.j2 | 10 - .../scripts/run_from_github_release.sh | 7 + .../updatecli.d/updatecli_github_commit.yml | 4 +- workspace/last_fetched_release.json | 2 +- 17 files changed, 1599 insertions(+), 78 deletions(-) create mode 100644 docker/docker-compose.with_volumes.yml create mode 100644 src/packages/github_releases/testdata/fc-release-with-asset.json create mode 100644 test/testdata.ts delete mode 100644 updatecli/new_stuff/updatecli.d/ansible/deploy.yml delete mode 100644 updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml delete mode 100644 updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 diff --git a/docker/Dockerfile b/docker/Dockerfile index d68c2b2..6f66910 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,24 @@ -FROM ubuntu:20.04 +FROM python:3 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update + +# RUN sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list && \ +# apt-get update && \ +# apt-get install -y curl RUN mkdir -p /workspace/app RUN mkdir -p /workspace/runners RUN mkdir -p /workspace/data + +RUN curl -sL -o /tmp/updatecli_amd64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_amd64.deb && \ + dpkg -i /tmp/updatecli_amd64.deb && \ + rm /tmp/updatecli_amd64.deb + +RUN python3 -m pip install --user ansible +# RUN updatecli version + WORKDIR /workspace/app COPY ./workspace/repo_config.json /workspace/repo_config.json diff --git a/docker/docker-compose.with_volumes.yml b/docker/docker-compose.with_volumes.yml new file mode 100644 index 0000000..cf1b5a2 --- /dev/null +++ b/docker/docker-compose.with_volumes.yml @@ -0,0 +1,10 @@ +version: '3' +services: + pi_with_volumes: + build: + context: . + network: none + command: ./index + platform: linux/x86_64 + volumes: + - "./workspace:/workspace" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 666b574..5d85f5d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,13 +1,20 @@ -version: '3' services: pi: build: context: . - network: none + # network: none command: ./index + # command: updatecli version platform: linux/x86_64 environment: WORKSPACE_DIRECTORY: '/workspace' ASSET_NAME: 'index-linux-x64' + GITHUB_API_URL: ${GITHUB_API_URL} + # networks: + # - app-network + # volumes: # - "./workspace:/workspace" +# networks: +# app-network: +# driver: bridge diff --git a/package-lock.json b/package-lock.json index 5d357cb..b31b11f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,10 @@ "@types/node": "^20.12.12", "@yao-pkg/pkg": "^5.11.5", "esbuild": "0.21.4", + "fullcircle": "git+https://github.com/fullcircle-testing/fullcircle.git#v0.1.0", "jest": "^29.7.0", "ts-jest": "^29.1.3", + "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } }, @@ -603,6 +605,28 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.4.tgz", @@ -971,6 +995,102 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1500,6 +1620,224 @@ "@octokit/openapi-types": "^22.2.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1524,6 +1862,30 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1565,6 +1927,64 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-http-proxy": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/express-http-proxy/-/express-http-proxy-1.6.6.tgz", + "integrity": "sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", + "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1574,6 +1994,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1608,7 +2034,13 @@ "pretty-format": "^29.0.0" } }, - "node_modules/@types/node": { + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { "version": "20.12.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", @@ -1617,12 +2049,67 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -1723,6 +2210,27 @@ "node": ">=6.9.0" } }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1774,6 +2282,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1787,6 +2301,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1805,6 +2325,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -1961,6 +2487,18 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2091,6 +2629,30 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bundle-require": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.2.1.tgz", + "integrity": "sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.17" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2154,6 +2716,30 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -2226,6 +2812,27 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2265,6 +2872,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2343,6 +2956,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -2366,6 +2988,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2387,6 +3018,21 @@ "node": ">=8" } }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.783", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", @@ -2620,6 +3266,48 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -2671,6 +3359,29 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fullcircle": { + "name": "fullcircle-root", + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/fullcircle-testing/fullcircle.git#ff20b8f7db91603ef3de4919eb07bfc03c580f26", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/express": "^4.17.21", + "@types/express-http-proxy": "^1.6.6", + "@types/node": "^20.9.2", + "@types/node-fetch": "^2.6.9", + "tsup": "^8.0.1", + "typescript": "^5.2.2" + }, + "bin": { + "fc-record": "dist/recorder.js" + }, + "workspaces": { + "packages": [ + "packages/*" + ] + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2953,6 +3664,18 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -3103,6 +3826,24 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.2.5.tgz", + "integrity": "sha512-a1hopwtr4NawFIrSmFgufzrN1Qy2BAfMJ0yScJBs/olJhTcctCy3YIDx4hTY2DOTJD1pUMTly80kmlYZxjZr5w==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -3711,6 +4452,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3790,12 +4540,33 @@ "node": ">=6" } }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3814,6 +4585,12 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3881,6 +4658,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3923,6 +4721,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -3973,6 +4792,17 @@ "node": ">= 6" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -4050,6 +4880,15 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4184,6 +5023,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4232,6 +5096,41 @@ "node": ">=8" } }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -4322,6 +5221,15 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4394,6 +5302,18 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4472,6 +5392,54 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4682,6 +5650,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4694,6 +5677,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -4721,6 +5717,74 @@ "node": ">=0.10.0" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4801,6 +5865,27 @@ "node": ">=8" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4834,6 +5919,21 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, "node_modules/ts-jest": { "version": "29.1.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.3.tgz", @@ -4890,6 +5990,191 @@ "node": ">=12" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsup": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.1.0.tgz", + "integrity": "sha512-UFdfCAXukax+U6KzeTNO2kAARHcWxmKsnvSPXUcfA1D+kU05XDccCrkffCQpFaWDsZfV0jMyTsxU39VfCp6EOg==", + "dev": true, + "dependencies": { + "bundle-require": "^4.0.0", + "cac": "^6.7.12", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.21.4", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^4.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.0.2", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.20.3", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tsup/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tsup/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/tsup/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -4992,6 +6277,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -5063,6 +6354,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5081,6 +6390,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5096,6 +6414,18 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", + "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -5123,6 +6453,15 @@ "node": ">=10" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index f1e6bb9..88b38c7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "build": "esbuild src/apps/update_checker/index.ts --bundle --outfile=dist/index.js --platform=node", + "dev": "ts-node-dev src/apps/update_checker/index.ts", "dist-pkg": "pkg -t node18-linux-x64,node18-macos-arm64 dist/index.js --out-path docker/dist-pkg", "test": "jest test", "test:ci": "npm test", @@ -18,8 +19,10 @@ "@types/node": "^20.12.12", "@yao-pkg/pkg": "^5.11.5", "esbuild": "0.21.4", + "fullcircle": "git+https://github.com/fullcircle-testing/fullcircle.git#v0.1.0", "jest": "^29.7.0", "ts-jest": "^29.1.3", + "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" }, "dependencies": { diff --git a/src/packages/github_releases/testdata/fc-release-with-asset.json b/src/packages/github_releases/testdata/fc-release-with-asset.json new file mode 100644 index 0000000..87b8d81 --- /dev/null +++ b/src/packages/github_releases/testdata/fc-release-with-asset.json @@ -0,0 +1,154 @@ +{ + "time": "2024-06-05T06:38:00.166Z", + "host": "https://api.github.com", + "requestMethod": "GET", + "requestPath": "/repos/jamtools/github-releases-test/releases/latest", + "responseBody": { + "url": "https://api.github.com/repos/jamtools/github-releases-test/releases/157680714", + "assets_url": "https://api.github.com/repos/jamtools/github-releases-test/releases/157680714/assets", + "upload_url": "https://uploads.github.com/repos/jamtools/github-releases-test/releases/157680714/assets{?name,label}", + "html_url": "https://github.com/jamtools/github-releases-test/releases/tag/v2", + "id": 157680714, + "author": { + "login": "mickmister", + "id": 6913320, + "node_id": "MDQ6VXNlcjY5MTMzMjA=", + "avatar_url": "https://avatars.githubusercontent.com/u/6913320?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mickmister", + "html_url": "https://github.com/mickmister", + "followers_url": "https://api.github.com/users/mickmister/followers", + "following_url": "https://api.github.com/users/mickmister/following{/other_user}", + "gists_url": "https://api.github.com/users/mickmister/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mickmister/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mickmister/subscriptions", + "organizations_url": "https://api.github.com/users/mickmister/orgs", + "repos_url": "https://api.github.com/users/mickmister/repos", + "events_url": "https://api.github.com/users/mickmister/events{/privacy}", + "received_events_url": "https://api.github.com/users/mickmister/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDOMBWyHM4JZgRK", + "tag_name": "v2", + "target_commitish": "main", + "name": "v2", + "draft": false, + "prerelease": false, + "created_at": "2024-05-27T19:22:02Z", + "published_at": "2024-05-27T19:45:05Z", + "assets": [ + { + "url": "https://api.github.com/repos/jamtools/github-releases-test/releases/assets/171886714", + "id": 171886714, + "node_id": "RA_kwDOMBWyHM4KPsh6", + "name": "index-linux-x64", + "label": null, + "uploader": { + "login": "mickmister", + "id": 6913320, + "node_id": "MDQ6VXNlcjY5MTMzMjA=", + "avatar_url": "https://avatars.githubusercontent.com/u/6913320?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mickmister", + "html_url": "https://github.com/mickmister", + "followers_url": "https://api.github.com/users/mickmister/followers", + "following_url": "https://api.github.com/users/mickmister/following{/other_user}", + "gists_url": "https://api.github.com/users/mickmister/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mickmister/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mickmister/subscriptions", + "organizations_url": "https://api.github.com/users/mickmister/orgs", + "repos_url": "https://api.github.com/users/mickmister/repos", + "events_url": "https://api.github.com/users/mickmister/events{/privacy}", + "received_events_url": "https://api.github.com/users/mickmister/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 51397841, + "download_count": 5, + "created_at": "2024-06-04T17:20:20Z", + "updated_at": "2024-06-04T17:20:58Z", + "browser_download_url": "https://github.com/jamtools/github-releases-test/releases/download/v2/index-linux-x64" + }, + { + "url": "https://api.github.com/repos/jamtools/github-releases-test/releases/assets/171886783", + "id": 171886783, + "node_id": "RA_kwDOMBWyHM4KPsi_", + "name": "index-macos-arm64", + "label": null, + "uploader": { + "login": "mickmister", + "id": 6913320, + "node_id": "MDQ6VXNlcjY5MTMzMjA=", + "avatar_url": "https://avatars.githubusercontent.com/u/6913320?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mickmister", + "html_url": "https://github.com/mickmister", + "followers_url": "https://api.github.com/users/mickmister/followers", + "following_url": "https://api.github.com/users/mickmister/following{/other_user}", + "gists_url": "https://api.github.com/users/mickmister/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mickmister/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mickmister/subscriptions", + "organizations_url": "https://api.github.com/users/mickmister/orgs", + "repos_url": "https://api.github.com/users/mickmister/repos", + "events_url": "https://api.github.com/users/mickmister/events{/privacy}", + "received_events_url": "https://api.github.com/users/mickmister/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 47268336, + "download_count": 3, + "created_at": "2024-06-04T17:20:58Z", + "updated_at": "2024-06-04T17:21:33Z", + "browser_download_url": "https://github.com/jamtools/github-releases-test/releases/download/v2/index-macos-arm64" + } + ], + "tarball_url": "https://api.github.com/repos/jamtools/github-releases-test/tarball/v2", + "zipball_url": "https://api.github.com/repos/jamtools/github-releases-test/zipball/v2", + "body": "" + }, + "requestHeaders": { + "host": "host.docker.internal:3003", + "connection": "keep-alive", + "accept": "application/vnd.github.v3+json", + "user-agent": "octokit-rest.js/20.1.1 octokit-core.js/5.2.0 Node.js/18.19.1 (linux; x64)", + "accept-language": "*", + "sec-fetch-mode": "cors", + "accept-encoding": "gzip, deflate" + }, + "responseHeaders": { + "server": "GitHub.com", + "date": "Wed, 05 Jun 2024 06:38:00 GMT", + "content-type": "application/json; charset=utf-8", + "cache-control": "public, max-age=60, s-maxage=60", + "vary": "Accept, Accept-Encoding, Accept, X-Requested-With", + "etag": "W/\"cf480c93f05efa105add88094062a42c9bf2bc5d7352dc0b9c8b09920a881242\"", + "last-modified": "Tue, 04 Jun 2024 17:21:33 GMT", + "x-github-media-type": "github.v3; format=json", + "x-github-api-version-selected": "2022-11-28", + "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", + "access-control-allow-origin": "*", + "strict-transport-security": "max-age=31536000; includeSubdomains; preload", + "x-frame-options": "deny", + "x-content-type-options": "nosniff", + "x-xss-protection": "0", + "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", + "content-security-policy": "default-src 'none'", + "content-encoding": "gzip", + "x-ratelimit-limit": "60", + "x-ratelimit-remaining": "59", + "x-ratelimit-reset": "1717573080", + "x-ratelimit-resource": "core", + "x-ratelimit-used": "1", + "accept-ranges": "bytes", + "content-length": "853", + "x-github-request-id": "FBAA:2149AA:6EEFCAD:C054EA0:666007C8", + "connection": "close" + }, + "requestIp": "::ffff:127.0.0.1", + "status": 200 + } diff --git a/src/packages/github_releases/testdata/first_release.json b/src/packages/github_releases/testdata/first_release.json index 1d3e8b4..f15c99b 100644 --- a/src/packages/github_releases/testdata/first_release.json +++ b/src/packages/github_releases/testdata/first_release.json @@ -6,8 +6,6 @@ "access-control-allow-origin": "*", "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", "cache-control": "public, max-age=60, s-maxage=60", - "content-encoding": "gzip", - "content-length": "570", "content-security-policy": "default-src 'none'", "content-type": "application/json; charset=utf-8", "date": "Mon, 27 May 2024 19:28:47 GMT", diff --git a/test/test.ts b/test/test.ts index 92fea9a..1f05ee0 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,13 +1,38 @@ import {exec} from 'child_process'; import {promisify} from 'util'; +import {fullcircle, TestHarness, FullCircleInstance} from 'fullcircle/dist/harness'; +import {ReleaseResponse} from '../src/packages/github_releases/release_fetcher'; + +import {TESTDATA} from './testdata'; + const execAsync = promisify(exec); const DEBUG = true; describe('yeah', () => { + let fc: FullCircleInstance; + let harness: TestHarness; + + beforeEach(async () => { + fc = await fullcircle({ + listenAddress: 1337, + defaultDestination: 'api.github.com', + verbose: false, + }); + + harness = fc.harness(''); + }); + + afterEach(async () => { + await fc.close(); + await harness.closeWithAssertions(); + }); + it('should work', async () => { - let out = await execAsync('npm run build'); + let out: Awaited>; + + out = await execAsync('npm run build'); if (DEBUG) {console.log(out.stdout); console.log(out.stderr);} out = await execAsync('npm run dist-pkg'); @@ -16,22 +41,28 @@ describe('yeah', () => { out = await execAsync('docker compose build', {cwd: './docker'}); if (DEBUG) {console.log(out.stdout); console.log(out.stderr);} - out = await execAsync('docker compose up', {cwd: './docker'}); + harness.get('/repos/jamtools/github-releases-test/releases/latest', (req, res) => { + res.json(TESTDATA.testDataFirstRelease); + }); + + // TODO: mock asset url download + + out = await execAsync('docker compose up', {cwd: './docker', env: {GITHUB_API_URL: 'http://host.docker.internal:1337'}}); if (DEBUG) {console.log(out.stdout); console.log(out.stderr);} const lines = getConsoleLinesFromDockerComposeStdOut('pi', out.stdout); expect(lines).toEqual([ 'new release found', - 'downloading release asset', - 'yup', + // 'downloading release asset', + // 'yup', ]); }, 50000); }); -const getConsoleLinesFromDockerComposeStdOut = (containerName: string, stdout: string) => { +const getConsoleLinesFromDockerComposeStdOut = (containerName: string, stdout: string | Buffer) => { const key = `${containerName}-1`; const toSearch = `${key} | `; - const lines = stdout.split('\n'); + const lines = stdout.toString().split('\n'); return lines.filter(l => l.includes(toSearch)).map(l => l.split(toSearch)[1]).filter(Boolean); } diff --git a/test/testdata.ts b/test/testdata.ts new file mode 100644 index 0000000..b041f9a --- /dev/null +++ b/test/testdata.ts @@ -0,0 +1,15 @@ +import testDataNoReleases from '../src/packages/github_releases/testdata/no_releases.json'; +import testDataFirstRelease from '../src/packages/github_releases/testdata/first_release.json'; +import testDataSecondRelease from '../src/packages/github_releases/testdata/second_release.json'; + +export const TESTDATA = { + testDataNoReleases, + testDataFirstRelease: { + ...testDataFirstRelease, + status: 200 as const, + }, + testDataSecondRelease: { + ...testDataSecondRelease, + status: 200 as const, + }, +}; diff --git a/tsconfig.json b/tsconfig.json index a4c4459..718426d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ "outDir": "dist", + "resolveJsonModule": true, /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ diff --git a/updatecli/Dockerfile b/updatecli/Dockerfile index d6e456d..ffd14c1 100644 --- a/updatecli/Dockerfile +++ b/updatecli/Dockerfile @@ -57,7 +57,9 @@ COPY new_stuff/updatecli.d/scripts/check_esbuild.js . # # COPY ./dist-pkg/index-linux-x64 ./index -RUN pip3 install ansible +RUN pip3 install ansible --break-system-packages + +RUN apt-get install --allow-downgrades --fix-missing -y cockpit VOLUME [ "/sys/fs/cgroup" ] STOPSIGNAL SIGRTMIN+3 diff --git a/updatecli/new_stuff/updatecli.d/ansible/deploy.yml b/updatecli/new_stuff/updatecli.d/ansible/deploy.yml deleted file mode 100644 index e80972b..0000000 --- a/updatecli/new_stuff/updatecli.d/ansible/deploy.yml +++ /dev/null @@ -1,11 +0,0 @@ -- hosts: localhost - connection: local - become: true - vars: - service_name: myapp - service_description: "Local Service" - artifact_path: "/etc/updatecli/artifacts/{{ artifact_name }}" - service_user: root - service_group: root - roles: - - deploy_artifact diff --git a/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml b/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml deleted file mode 100644 index 37b4b1f..0000000 --- a/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/tasks/main.yml +++ /dev/null @@ -1,41 +0,0 @@ -- name: Install binary - copy: - src: "{{ artifact_path }}" - dest: "/etc/updatecli/{{ artifact_name }}" - mode: '0755' - -- name: Create systemd service file - template: - src: service.j2 - dest: "/etc/systemd/system/{{ service_name }}.service" - mode: '0644' - -- name: Reload systemd daemon - ansible.builtin.systemd: - daemon_reload: true - - -- name: Enable and start the service - block: - - - name: Enable and restart the service - ansible.builtin.systemd: - name: "{{ service_name }}" - enabled: true - state: restarted - - rescue: - - - name: Fetch service logs after failure - shell: | - journalctl -u {{ service_name }}.service --no-pager -n 50 || echo "No logs found" - register: service_logs - ignore_errors: true - - - name: Display service logs - debug: - var: service_logs.stdout_lines - - - name: Fail with context - fail: - msg: "Service {{ service_name }} failed to start. See logs above." diff --git a/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 b/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 deleted file mode 100644 index 35c307f..0000000 --- a/updatecli/new_stuff/updatecli.d/ansible/roles/deploy_artifact/templates/service.j2 +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description={{ service_description }} -After=network.target - -[Service] -ExecStart=/usr/bin/node /etc/updatecli/{{ artifact_name }} -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh index cb18e78..c3f2906 100755 --- a/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh +++ b/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh @@ -9,3 +9,10 @@ mkdir -p /home/jamtools/code/artifacts/dist unzip -o /home/jamtools/code/artifacts/dist.zip -d /home/jamtools/code/artifacts/dist /home/jamtools/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/jamtools/code/artifacts/dist/server/dist/local-server.cjs" + +source /home/jamtools/code/secrets.env + +if [ -n "${WEBHOOK_URL}" ]; then + echo "Sending webhook notification" + curl -X POST -H "Content-Type: application/json" -d '{"text":"new release deployed"}' "${WEBHOOK_URL}" +fi diff --git a/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml index 8ef50af..c142b28 100644 --- a/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml +++ b/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml @@ -5,7 +5,7 @@ sources: # name: github version spec: owner: jamtools - repository: songdrive-releases + repository: jamscribe token: "{{ requiredEnv `GITHUB_TOKEN` }}" versionfilter: kind: regex @@ -23,4 +23,4 @@ targets: kind: shell sourceid: newRelease spec: - command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools songdrive-releases + command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools jamscribe diff --git a/workspace/last_fetched_release.json b/workspace/last_fetched_release.json index bb6e1f6..a046102 100644 --- a/workspace/last_fetched_release.json +++ b/workspace/last_fetched_release.json @@ -1 +1 @@ -{"status":200,"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/latest","headers":{"accept-ranges":"bytes","access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","cache-control":"public, max-age=60, s-maxage=60","content-encoding":"gzip","content-length":"848","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Tue, 04 Jun 2024 17:42:04 GMT","etag":"W/\"98022bd4bdfb04f7344bc88ff2b3751d50fb58a4db71e549e9da7f6d42198526\"","last-modified":"Tue, 04 Jun 2024 17:21:33 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"GitHub.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept, Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"E6A5:22F369:44D63E2:7A4ABCA:665F51F2","x-ratelimit-limit":"60","x-ratelimit-remaining":"56","x-ratelimit-reset":"1717525152","x-ratelimit-resource":"core","x-ratelimit-used":"4","x-xss-protection":"0"},"data":{"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/157680714","assets_url":"https://api.github.com/repos/jamtools/github-releases-test/releases/157680714/assets","upload_url":"https://uploads.github.com/repos/jamtools/github-releases-test/releases/157680714/assets{?name,label}","html_url":"https://github.com/jamtools/github-releases-test/releases/tag/v2","id":157680714,"author":{"login":"mickmister","id":6913320,"node_id":"MDQ6VXNlcjY5MTMzMjA=","avatar_url":"https://avatars.githubusercontent.com/u/6913320?v=4","gravatar_id":"","url":"https://api.github.com/users/mickmister","html_url":"https://github.com/mickmister","followers_url":"https://api.github.com/users/mickmister/followers","following_url":"https://api.github.com/users/mickmister/following{/other_user}","gists_url":"https://api.github.com/users/mickmister/gists{/gist_id}","starred_url":"https://api.github.com/users/mickmister/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mickmister/subscriptions","organizations_url":"https://api.github.com/users/mickmister/orgs","repos_url":"https://api.github.com/users/mickmister/repos","events_url":"https://api.github.com/users/mickmister/events{/privacy}","received_events_url":"https://api.github.com/users/mickmister/received_events","type":"User","site_admin":false},"node_id":"RE_kwDOMBWyHM4JZgRK","tag_name":"v2","target_commitish":"main","name":"v2","draft":false,"prerelease":false,"created_at":"2024-05-27T19:22:02Z","published_at":"2024-05-27T19:45:05Z","assets":[{"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/assets/171886714","id":171886714,"node_id":"RA_kwDOMBWyHM4KPsh6","name":"index-linux-x64","label":null,"uploader":{"login":"mickmister","id":6913320,"node_id":"MDQ6VXNlcjY5MTMzMjA=","avatar_url":"https://avatars.githubusercontent.com/u/6913320?v=4","gravatar_id":"","url":"https://api.github.com/users/mickmister","html_url":"https://github.com/mickmister","followers_url":"https://api.github.com/users/mickmister/followers","following_url":"https://api.github.com/users/mickmister/following{/other_user}","gists_url":"https://api.github.com/users/mickmister/gists{/gist_id}","starred_url":"https://api.github.com/users/mickmister/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mickmister/subscriptions","organizations_url":"https://api.github.com/users/mickmister/orgs","repos_url":"https://api.github.com/users/mickmister/repos","events_url":"https://api.github.com/users/mickmister/events{/privacy}","received_events_url":"https://api.github.com/users/mickmister/received_events","type":"User","site_admin":false},"content_type":"application/octet-stream","state":"uploaded","size":51397841,"download_count":0,"created_at":"2024-06-04T17:20:20Z","updated_at":"2024-06-04T17:20:58Z","browser_download_url":"https://github.com/jamtools/github-releases-test/releases/download/v2/index-linux-x64"},{"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/assets/171886783","id":171886783,"node_id":"RA_kwDOMBWyHM4KPsi_","name":"index-macos-arm64","label":null,"uploader":{"login":"mickmister","id":6913320,"node_id":"MDQ6VXNlcjY5MTMzMjA=","avatar_url":"https://avatars.githubusercontent.com/u/6913320?v=4","gravatar_id":"","url":"https://api.github.com/users/mickmister","html_url":"https://github.com/mickmister","followers_url":"https://api.github.com/users/mickmister/followers","following_url":"https://api.github.com/users/mickmister/following{/other_user}","gists_url":"https://api.github.com/users/mickmister/gists{/gist_id}","starred_url":"https://api.github.com/users/mickmister/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mickmister/subscriptions","organizations_url":"https://api.github.com/users/mickmister/orgs","repos_url":"https://api.github.com/users/mickmister/repos","events_url":"https://api.github.com/users/mickmister/events{/privacy}","received_events_url":"https://api.github.com/users/mickmister/received_events","type":"User","site_admin":false},"content_type":"application/octet-stream","state":"uploaded","size":47268336,"download_count":0,"created_at":"2024-06-04T17:20:58Z","updated_at":"2024-06-04T17:21:33Z","browser_download_url":"https://github.com/jamtools/github-releases-test/releases/download/v2/index-macos-arm64"}],"tarball_url":"https://api.github.com/repos/jamtools/github-releases-test/tarball/v2","zipball_url":"https://api.github.com/repos/jamtools/github-releases-test/zipball/v2","body":""}} \ No newline at end of file +{"status":200,"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/latest","headers":{"accept-ranges":"bytes","access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","cache-control":"public, max-age=60, s-maxage=60","content-encoding":"gzip","content-length":"854","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Tue, 11 Jun 2024 05:10:09 GMT","etag":"W/\"239bf7f29aae6ade76c1612cd5f9fb27606cb717d4ae61aa330330a4c3b5a95d\"","last-modified":"Tue, 04 Jun 2024 17:21:33 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"GitHub.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept, Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"DDDF:37E569:CB5171:147C7FE:6667DC36","x-ratelimit-limit":"60","x-ratelimit-remaining":"58","x-ratelimit-reset":"1718086209","x-ratelimit-resource":"core","x-ratelimit-used":"2","x-xss-protection":"0"},"data":{"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/157680714","assets_url":"https://api.github.com/repos/jamtools/github-releases-test/releases/157680714/assets","upload_url":"https://uploads.github.com/repos/jamtools/github-releases-test/releases/157680714/assets{?name,label}","html_url":"https://github.com/jamtools/github-releases-test/releases/tag/v2","id":157680714,"author":{"login":"mickmister","id":6913320,"node_id":"MDQ6VXNlcjY5MTMzMjA=","avatar_url":"https://avatars.githubusercontent.com/u/6913320?v=4","gravatar_id":"","url":"https://api.github.com/users/mickmister","html_url":"https://github.com/mickmister","followers_url":"https://api.github.com/users/mickmister/followers","following_url":"https://api.github.com/users/mickmister/following{/other_user}","gists_url":"https://api.github.com/users/mickmister/gists{/gist_id}","starred_url":"https://api.github.com/users/mickmister/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mickmister/subscriptions","organizations_url":"https://api.github.com/users/mickmister/orgs","repos_url":"https://api.github.com/users/mickmister/repos","events_url":"https://api.github.com/users/mickmister/events{/privacy}","received_events_url":"https://api.github.com/users/mickmister/received_events","type":"User","site_admin":false},"node_id":"RE_kwDOMBWyHM4JZgRK","tag_name":"v2","target_commitish":"main","name":"v2","draft":false,"prerelease":false,"created_at":"2024-05-27T19:22:02Z","published_at":"2024-05-27T19:45:05Z","assets":[{"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/assets/171886714","id":171886714,"node_id":"RA_kwDOMBWyHM4KPsh6","name":"index-linux-x64","label":null,"uploader":{"login":"mickmister","id":6913320,"node_id":"MDQ6VXNlcjY5MTMzMjA=","avatar_url":"https://avatars.githubusercontent.com/u/6913320?v=4","gravatar_id":"","url":"https://api.github.com/users/mickmister","html_url":"https://github.com/mickmister","followers_url":"https://api.github.com/users/mickmister/followers","following_url":"https://api.github.com/users/mickmister/following{/other_user}","gists_url":"https://api.github.com/users/mickmister/gists{/gist_id}","starred_url":"https://api.github.com/users/mickmister/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mickmister/subscriptions","organizations_url":"https://api.github.com/users/mickmister/orgs","repos_url":"https://api.github.com/users/mickmister/repos","events_url":"https://api.github.com/users/mickmister/events{/privacy}","received_events_url":"https://api.github.com/users/mickmister/received_events","type":"User","site_admin":false},"content_type":"application/octet-stream","state":"uploaded","size":51397841,"download_count":6,"created_at":"2024-06-04T17:20:20Z","updated_at":"2024-06-04T17:20:58Z","browser_download_url":"https://github.com/jamtools/github-releases-test/releases/download/v2/index-linux-x64"},{"url":"https://api.github.com/repos/jamtools/github-releases-test/releases/assets/171886783","id":171886783,"node_id":"RA_kwDOMBWyHM4KPsi_","name":"index-macos-arm64","label":null,"uploader":{"login":"mickmister","id":6913320,"node_id":"MDQ6VXNlcjY5MTMzMjA=","avatar_url":"https://avatars.githubusercontent.com/u/6913320?v=4","gravatar_id":"","url":"https://api.github.com/users/mickmister","html_url":"https://github.com/mickmister","followers_url":"https://api.github.com/users/mickmister/followers","following_url":"https://api.github.com/users/mickmister/following{/other_user}","gists_url":"https://api.github.com/users/mickmister/gists{/gist_id}","starred_url":"https://api.github.com/users/mickmister/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mickmister/subscriptions","organizations_url":"https://api.github.com/users/mickmister/orgs","repos_url":"https://api.github.com/users/mickmister/repos","events_url":"https://api.github.com/users/mickmister/events{/privacy}","received_events_url":"https://api.github.com/users/mickmister/received_events","type":"User","site_admin":false},"content_type":"application/octet-stream","state":"uploaded","size":47268336,"download_count":3,"created_at":"2024-06-04T17:20:58Z","updated_at":"2024-06-04T17:21:33Z","browser_download_url":"https://github.com/jamtools/github-releases-test/releases/download/v2/index-macos-arm64"}],"tarball_url":"https://api.github.com/repos/jamtools/github-releases-test/tarball/v2","zipball_url":"https://api.github.com/repos/jamtools/github-releases-test/zipball/v2","body":""}} \ No newline at end of file From e8220dfe54f077970ee329bd078fbdaea72de67b Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:03:09 -0400 Subject: [PATCH 22/56] add FullPageOS --- FullPageOS/.github/ISSUE_TEMPLATE.md | 19 + FullPageOS/.github/workflows/main.yml | 37 + FullPageOS/.gitignore | 10 + FullPageOS/LICENSE | 674 ++++++++++++++++++ FullPageOS/README.rst | 158 ++++ FullPageOS/media/FullPageOS.png | Bin 0 -> 41207 bytes FullPageOS/media/FullPageOS.svg | 156 ++++ FullPageOS/media/rpi-imager-FullPageOS.png | Bin 0 -> 3893 bytes FullPageOS/src/build_dist | 9 + FullPageOS/src/config | 18 + FullPageOS/src/image/README | 5 + FullPageOS/src/modules/fullpageos/config | 41 ++ .../filesystem/boot/fullpagedashboard.txt | 1 + .../fullpageos/filesystem/boot/fullpageos.txt | 1 + .../fullpageos/filesystem/boot/splash.png | Bin 0 -> 112610 bytes .../filesystem/opt/custompios/background.png | Bin 0 -> 112610 bytes .../opt/custompios/scripts/fullscreen | 6 + .../filesystem/opt/custompios/scripts/get_url | 5 + .../filesystem/opt/custompios/scripts/refresh | 6 + .../custompios/scripts/reload_fullpageos_txt | 2 + .../opt/custompios/scripts/run_onepageos | 14 + .../opt/custompios/scripts/safe_refresh | 18 + .../opt/custompios/scripts/setX11vncPass | 4 + .../custompios/scripts/start_chromium_browser | 34 + .../system/clear_lighttpd_cache.service | 8 + .../etc/systemd/system/splashscreen.service | 12 + .../etc/systemd/system/x11vnc.service | 13 + .../modules/fullpageos/start_chroot_script | 175 +++++ FullPageOS/src/vagrant/Vagrantfile | 17 + FullPageOS/src/vagrant/instructions.rst | 9 + FullPageOS/src/vagrant/run_vagrant_build.sh | 3 + FullPageOS/src/vagrant/setup.sh | 4 + .../src/variants/no-acceleration/config | 1 + 33 files changed, 1460 insertions(+) create mode 100644 FullPageOS/.github/ISSUE_TEMPLATE.md create mode 100644 FullPageOS/.github/workflows/main.yml create mode 100644 FullPageOS/.gitignore create mode 100644 FullPageOS/LICENSE create mode 100644 FullPageOS/README.rst create mode 100644 FullPageOS/media/FullPageOS.png create mode 100644 FullPageOS/media/FullPageOS.svg create mode 100644 FullPageOS/media/rpi-imager-FullPageOS.png create mode 100755 FullPageOS/src/build_dist create mode 100755 FullPageOS/src/config create mode 100644 FullPageOS/src/image/README create mode 100644 FullPageOS/src/modules/fullpageos/config create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/boot/fullpagedashboard.txt create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/boot/fullpageos.txt create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/boot/splash.png create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/background.png create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/fullscreen create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/get_url create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/refresh create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/reload_fullpageos_txt create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/run_onepageos create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/safe_refresh create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/setX11vncPass create mode 100755 FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/clear_lighttpd_cache.service create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/splashscreen.service create mode 100644 FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/x11vnc.service create mode 100755 FullPageOS/src/modules/fullpageos/start_chroot_script create mode 100644 FullPageOS/src/vagrant/Vagrantfile create mode 100644 FullPageOS/src/vagrant/instructions.rst create mode 100755 FullPageOS/src/vagrant/run_vagrant_build.sh create mode 100644 FullPageOS/src/vagrant/setup.sh create mode 100755 FullPageOS/src/variants/no-acceleration/config diff --git a/FullPageOS/.github/ISSUE_TEMPLATE.md b/FullPageOS/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..9b6f4c6 --- /dev/null +++ b/FullPageOS/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ +#### What were you doing? + +[Please be as specific as possible here] + +#### What did you expect to happen? + +#### What happened instead? + +#### Was there an error message displayed? What did it say? + +#### Version of FullPageOS? + +[Can be found in /etc/fullpageos_version ALWAYS INCLUDE.] + +#### Screenshot(s) showing the problem: + +[If applicable. Always include if unsure or reporting UI issues.] + +#### If you are building FullPageOS - provide a build.log that is created for the build diff --git a/FullPageOS/.github/workflows/main.yml b/FullPageOS/.github/workflows/main.yml new file mode 100644 index 0000000..caef7b2 --- /dev/null +++ b/FullPageOS/.github/workflows/main.yml @@ -0,0 +1,37 @@ +name: Build Image + +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Update apt + run: sudo apt-get update + - name: Install Dependencies + run: sudo apt install coreutils p7zip-full qemu-user-static python3-git + - name: Checkout CustomPiOS + uses: actions/checkout@v2 + with: + repository: 'guysoft/CustomPiOS' + path: CustomPiOS + - name: Checkout Project Repository + uses: actions/checkout@v2 + with: + repository: ${{ github.repository }} + path: repository + submodules: true + - name: Download Raspbian Image + run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' + - name: Update CustomPiOS Paths + run: cd repository/src && ../../CustomPiOS/src/update-custompios-paths + - name: Build Image + run: sudo modprobe loop && cd repository/src && sudo bash -x ./build_dist + - name: Copy Output + run: cp ${{ github.workspace }}/repository/src/workspace/*-raspios-*-lite.img build.img + - name: Zip Output + run: gzip build.img + - uses: actions/upload-artifact@v4 + with: + name: build.img.gz + path: build.img.gz diff --git a/FullPageOS/.gitignore b/FullPageOS/.gitignore new file mode 100644 index 0000000..ee3ff5a --- /dev/null +++ b/FullPageOS/.gitignore @@ -0,0 +1,10 @@ +src/config.local +src/image/*.zip +src/custompios_path +src/build.log +src/vagrant/.vagrant/* +src/vagrant/*.log +src/workspace/* +src/workspace*/* +src/variants/* +!src/variants/no-acceleration diff --git a/FullPageOS/LICENSE b/FullPageOS/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/FullPageOS/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/FullPageOS/README.rst b/FullPageOS/README.rst new file mode 100644 index 0000000..226a3ec --- /dev/null +++ b/FullPageOS/README.rst @@ -0,0 +1,158 @@ +FullPageOS +========== + +.. image:: https://github.com/guysoft/FullPageOS/blob/secularstevelogo/media/FullPageOS.png?raw=true +.. :scale: 50 % +.. :alt: FullPageOS logo + +A `Raspberry Pi `_ distribution to display one webpage in full screen. It includes `Chromium `_ out of the box and the scripts necessary to load it at boot. +This repository contains the source script to generate the distribution out of an existing `Raspbian `_ distro image. + +FullPageOS started as a fork from `OctoPi `_, but then joined the distros that use `CustomPiOS `_. + +Donate +------ +FullPageOS is 100% free and open source and maintained by Guy Sheffer. If it's helping your life, your organisation or makes you happy, please consider making a donation. It means I can code more and worry less about my balance. Any amount counts. + +|paypal| + +.. |paypal| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif + :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=26VJ9MSBH3V3W&source=url + +Where to get it? +---------------- + +The official mirror is `here `_ + +Nightly builds are available `here `_ (currently built on demand) + +How to use it? +-------------- + +#. Unzip the image and install it to an SD card `like any other Raspberry Pi image `_ +#. Configure your WiFi by editing ``wifi.nmconnection`` on the first partition of the flashed card when using it like a flash drive +#. Boot the Pi from the SD card +#. Log into your Pi via SSH (it is located at ``fullpageos.local`` `if your computer supports bonjour `_ or the IP address assigned by your router), default username is "pi", default password is "raspberry" and change the password using the ``passwd`` command. Consider also changing the vnc password as well by `x11vnc -storepasswd`. + +Requirements +------------ +* Raspberry Pi 2 and newer or device running Armbian. Older Raspberry Pis are not currently supported. See `Raspberry Pi `_ and `Raspberry Pi `_. +* SD card, 4GB or larger, Class 10. (Early June 2020 was the image size 3GB.) +* 2A power supply + + +Features +-------- + +* Loads Chromium at boot in full screen +* Webpage can be changed from /boot/firmware/fullpageos.txt + * You can use variable `{serial}` in the url to get device's serialnumber in the URL +* Default app is `FullPageDashboard `_, which lets you add multiple tabs changes that switch automatically. +* Ships with preconfigured `X11VNC `_, for remote connection (password 'raspberry') +* Specify a custom Splashscreen that gets displayed in the booting process instead of Kernel messages/text + +Developing +---------- + +Requirements +~~~~~~~~~~~~ + +#. `qemu-arm-static `_ +#. `CustomPiOS `_ +#. Downloaded `Raspbian `_ image. +#. root privileges for chroot +#. Bash +#. realpath +#. sudo (the script itself calls it, running as root without sudo won't work) +#. jq (part of CustomPiOS dependencies) + +Build FullPageOS From within FullPageOS / Raspbian / Debian / Ubuntu +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FullPageOS can be built from Debian, Ubuntu, Raspbian, or even FullPageOS. +Build requires about 2.5 GB of free space available. +You can build it by issuing the following commands:: + + sudo apt install coreutils p7zip-full qemu-user-static + + git clone https://github.com/guysoft/CustomPiOS.git + git clone https://github.com/guysoft/FullPageOS.git + cd FullPageOS/src/image + wget -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' + cd .. + ../../CustomPiOS/src/update-custompios-paths + sudo modprobe loop + sudo bash -x ./build_dist + +Building FullPageOS Variants +~~~~~~~~~~~~~~~~~~~~~~~~ + +FullPageOS supports building variants, which are builds with changes from the main release build. An example and other variants are available in the folder ``src/variants/example``. + +To build a variant use:: + + sudo bash -x ./build_dist [Variant] + + +Building Using Docker +~~~~~~~~~~~~~~~~~~~~~~ +`See Building with docker entry in wiki `_ + + +Building Using Vagrant +~~~~~~~~~~~~~~~~~~~~~~ +There is a vagrant machine configuration to let build FullPageOS in case your build environment behaves differently. Unless you do extra configuration, vagrant must run as root to have nfs folder sync working. + +Make sure you have a version of vagrant later than 1.9! + +If you are using older versions of Ubuntu/Debian and not using apt-get `from the download page `_. + +To use it:: + + sudo apt-get install vagrant nfs-kernel-server virtualbox + sudo vagrant plugin install vagrant-nfs_guest + sudo modprobe nfs + cd FullPageOS/src/vagrant + sudo vagrant up + +After provisioning the machine, it's also possible to run a nightly build which updates from devel using:: + + cd FullPageOS/src/vagrant + run_vagrant_build.sh + +To build a variant on the machine simply run:: + + cd FullPageOS/src/vagrant + run_vagrant_build.sh [Variant] + +Usage +~~~~~ + +#. If needed, override existing config settings by creating a new file ``src/config.local``. You can override all settings found in ``src/config``. If you need to override the path to the Raspbian image to use for building OctoPi, override the path to be used in ``ZIP_IMG``. By default, the most recent file matching ``*-raspbian.zip`` found in ``src/image`` will be used. +#. Run ``src/build_dist`` as root. +#. The final image will be created in ``src/workspace`` + + +Remote access +~~~~~~~~~~~~~ + +Remote GUI access can be achieved through VNC Viewer. Get the IP of your raspberry ``hostname -I`` via SSH. + +The password is ``raspberry`` and is independent of password you have set for your user(s). Change the password by ``x11vnc -storepasswd`` via SSH. + + +Install Chrome Extensions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Press ``ctrl`` + ``t``, it will open a new tab. + +You can either install extensions from `Chrome Web Store `_ or `install your own extension `_. + +If you wish to install your own extension then you can transfer the build files via tools like ``rcp``, ``rsync`` etc. + +Example:: + + rsync -av / pi@fullpageos.local:extensions// + + +Code contribution would be appreciated! diff --git a/FullPageOS/media/FullPageOS.png b/FullPageOS/media/FullPageOS.png new file mode 100644 index 0000000000000000000000000000000000000000..bb093a2fcef943b0449b20b4a852d020e09b9efa GIT binary patch literal 41207 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�fHX0I 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 TI){-o)C0b0VO(o?Kjwb`*%0e; literal 0 HcmV?d00001 diff --git a/FullPageOS/media/FullPageOS.svg b/FullPageOS/media/FullPageOS.svg new file mode 100644 index 0000000..692c377 --- /dev/null +++ b/FullPageOS/media/FullPageOS.svg @@ -0,0 +1,156 @@ + + + + diff --git a/FullPageOS/media/rpi-imager-FullPageOS.png b/FullPageOS/media/rpi-imager-FullPageOS.png new file mode 100644 index 0000000000000000000000000000000000000000..69ff5dbab82e41b184789fa4bc836059255933a4 GIT binary patch literal 3893 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
  • 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

    #L1ern;*2;j?nW6QR!2=0`1CK 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 S2Ged-8fP!6Wu39S`~LvMK4x10 literal 0 HcmV?d00001 diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/background.png b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/background.png new file mode 100644 index 0000000000000000000000000000000000000000..c75fc01e80e41dce6c8237be46273c9b7843f4bf GIT binary patch literal 112610 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

  • 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

    #L1ern;*2;j?nW6QR!2=0`1CK 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 S2Ged-8fP!6Wu39S`~LvMK4x10 literal 0 HcmV?d00001 diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/fullscreen b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/fullscreen new file mode 100755 index 0000000..a49ffad --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/fullscreen @@ -0,0 +1,6 @@ +#!/bin/bash +export DISPLAY=:0 +WID=$(xdotool search --onlyvisible --class chromium|head -1) +xdotool windowactivate ${WID} +xdotool key F11 +xdotool mousemove 9000 9000 diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/get_url b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/get_url new file mode 100755 index 0000000..33dc323 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/get_url @@ -0,0 +1,5 @@ +#!/bin/bash + +SERIAL=`cat /proc/cpuinfo | grep -i '^Serial' | awk '{ print $3 }'` +URL="$(head -n 1 /boot/firmware/fullpageos.txt | sed -e "s/{serial}/${SERIAL}/g")" +echo $URL diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/refresh b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/refresh new file mode 100755 index 0000000..a7f7b68 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/refresh @@ -0,0 +1,6 @@ +#!/bin/bash +export DISPLAY=:0 +sleep 1 +WID=$(xdotool search --onlyvisible --class chromium|head -1) +xdotool windowactivate ${WID} +xdotool key ctrl+F5 diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/reload_fullpageos_txt b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/reload_fullpageos_txt new file mode 100755 index 0000000..e23b090 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/reload_fullpageos_txt @@ -0,0 +1,2 @@ +#!/bin/bash +killall /usr/lib/chromium/chromium diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/run_onepageos b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/run_onepageos new file mode 100755 index 0000000..9e721dc --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/run_onepageos @@ -0,0 +1,14 @@ +#!/bin/bash + +USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + +feh --bg-center /opt/custompios/background.png + +while true +do + if [[ $(curl -sL -b cookiefile -w "%{http_code}\\n" -H "user-agent: ${USER_AGENT}" "$(/opt/custompios/scripts/get_url)" -o /dev/null) =~ ^([23][0-9]{2,2}|401)$ ]] || grep -q disabled "/boot/firmware/check_for_httpd" ; then + xdotool mousemove 9000 9000 + %BROWSER_START_SCRIPT% + fi + sleep 1 +done diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/safe_refresh b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/safe_refresh new file mode 100644 index 0000000..376efe3 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/safe_refresh @@ -0,0 +1,18 @@ +#!/bin/bash + +# This script attempts a refresh, but only if the requested page returns successfully. + +curl --fail "$(/opt/custompios/scripts/get_url)" &> /dev/null +curlRetr="$?" + +if [[ "$curlRetr" -ne "0" ]]; then + echo "Waiting to refresh since server is having issues."; + exit 1; +fi + +echo "Refreshing page."; + +export DISPLAY=:0 +WID=$(xdotool search --onlyvisible --class chromium|head -1) +xdotool windowactivate ${WID} +xdotool key ctrl+F5 diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/setX11vncPass b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/setX11vncPass new file mode 100755 index 0000000..c76d809 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/setX11vncPass @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# fix for when runnign in sudo +bash -c 'mkdir -p /opt/custompios/vnc' +bash -c 'x11vnc -storepasswd "'${1}'" /opt/custompios/vnc/passwd' diff --git a/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser new file mode 100755 index 0000000..be5eafa --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/opt/custompios/scripts/start_chromium_browser @@ -0,0 +1,34 @@ +#!/bin/bash + +flags=( + --kiosk + --touch-events=enabled + --disable-pinch + --noerrdialogs + --disable-session-crashed-bubble + --simulate-outdated-no-au='Tue, 31 Dec 2099 23:59:59 GMT' + --disable-component-update + --overscroll-history-navigation=0 + --disable-features=TranslateUI + --autoplay-policy=no-user-gesture-required +) + +# Standard behavior - runs chromium +chromium-browser "${flags[@]}" --app=$(/opt/custompios/scripts/get_url) +exit; + +# Remove the two lines above to enable signage mode - refresh the browser whenever errors are seen in log + +chromium-browser --enable-logging --log-level=2 --v=0 "${flags[@]}" --app=$(/opt/custompios/scripts/get_url) & + +export logfile="/home/$(id -nu 1000)/.config/chromium/chrome_debug.log" + + +# Refreshes after a crash by watching logs +tail -n 0 -F "$logfile" | while read LOGLINE &> /dev/null; do + + echo "Refreshing after crash" + echo "Restarting at `date` after a reported crash. Logline: ${LOGLINE}" >> /tmp/crashlog + [[ ("${LOGLINE}" == *"ERROR"*) || ("${LOGLINE}" == *"FATAL"*) ]] && /home/$(id -nu 1000)/scripts/refresh + +done diff --git a/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/clear_lighttpd_cache.service b/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/clear_lighttpd_cache.service new file mode 100644 index 0000000..1552041 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/clear_lighttpd_cache.service @@ -0,0 +1,8 @@ +[Unit] +Description=Clear the cache of lighttpd when the system starts +[Service] +ExecStart=/bin/rm -rf /var/cache/lighttpd/* +ExecReload=/bin/rm -rf /var/cache/lighttpd/* +ExecStop= +[Install] +WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/splashscreen.service b/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/splashscreen.service new file mode 100644 index 0000000..5f67cf1 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/splashscreen.service @@ -0,0 +1,12 @@ +[Unit] +Description=Splash screen +DefaultDependencies=no +After=local-fs.target + +[Service] +ExecStart=/usr/bin/fbi -d /dev/fb0 --noverbose -a /boot/firmware/splash.png +StandardInput=tty +StandardOutput=tty + +[Install] +WantedBy=sysinit.target diff --git a/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/x11vnc.service b/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/x11vnc.service new file mode 100644 index 0000000..3b7fd67 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/filesystem/root_init/etc/systemd/system/x11vnc.service @@ -0,0 +1,13 @@ +[Unit] +Description=VNC Server for X11 +ConditionPathExists=/opt/custompios/vnc/passwd +Requires=display-manager.service + +[Service] +ExecStart=/usr/bin/x11vnc -display :0 -auth guess -many -noxdamage -rfbauth /opt/custompios/vnc/passwd -rfbport 5900 -shared +ExecStop=/usr/bin/x11vnc -R stop +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/fullpageos/start_chroot_script b/FullPageOS/src/modules/fullpageos/start_chroot_script new file mode 100755 index 0000000..ce960f8 --- /dev/null +++ b/FullPageOS/src/modules/fullpageos/start_chroot_script @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# FullPageOS generation script +# Helper script that runs in a Raspbian/others chroot to create the FullPageOS distro +# Written by Guy Sheffer +# GPL V3 +######## +set -x +set -e + +source /common.sh + +unpack /filesystem/opt /opt +unpack /filesystem/boot /"${BASE_BOOT_MOUNT_PATH}" +unpack /filesystem/root_init / + +apt-get update + +# Display a custom Splashscreen when booting the Rpi +if [ "$FULLPAGEOS_CUSTOM_SPLASHSCREEN" == "yes" ] +then + apt-get install -y fbi + if [ "${BASE_BOARD}" == raspberrypi* ]; then + sed -i 's/$/ logo.nologo consoleblank=0 loglevel=0 quiet/' /"${BASE_BOOT_MOUNT_PATH}"/cmdline.txt + fi + echo "disable_splash=1" >> /"${BASE_BOOT_MOUNT_PATH}"/config.txt + systemctl enable splashscreen.service + systemctl disable getty@tty1 +fi + +remove_extra=$(remove_if_installed scratch squeak-plugins-scratch squeak-vm python-minecraftpi minecraft-pi sonic-pi oracle-java8-jdk bluej greenfoot libreoffice-common libreoffice-core freepats) + +apt-get remove -y --purge $remove_extra + +apt-get autoremove -y + +#apt-get tools +apt-get -y --force-yes install git screen checkinstall avahi-daemon libavahi-compat-libdnssd1 xterm xdotool vim expect feh pulseaudio + +if [ "$FULLPAGEOS_INCLUDE_CHROMIUM" == "yes" ] +then + apt-get install -y --force-yes chromium-browser + sed -i 's@%BROWSER_START_SCRIPT%@/opt/custompios/scripts/start_chromium_browser@g' /opt/custompios/scripts/run_onepageos +fi + +#Install web stack +if [ "$FULLPAGEOS_INCLUDE_LIGHTTPD" == "yes" ] +then + apt-get install -y lighttpd php-common php-cgi php + lighty-enable-mod fastcgi-php + #service lighttpd force-reload + chown -R www-data:www-data /var/www/html + chmod 775 /var/www/html + usermod -a -G www-data pi + systemctl enable clear_lighttpd_cache.service + pushd /var/www/html + #Put git clones in place + if [ "$FULLPAGEOS_INCLUDE_DASHBOARD" == "yes" ] + then + gitclone FULLPAGEOS_DASHBOARD_REPO FullPageDashboard + chown -R pi:pi FullPageDashboard + chown -R www-data:www-data FullPageDashboard + chmod 775 FullPageDashboard + pushd FullPageDashboard + sed -i "s@'INIT_URL_PATH', __DIR__ . '/init.txt'@'INIT_URL_PATH', '/"${BASE_BOOT_MOUNT_PATH}"/fullpagedashboard.txt'@g" config.php + popd + fi + #Set Welcome screen + if [ "$FULLPAGEOS_INCLUDE_WELCOME" == "yes" ] + then + gitclone FULLPAGEOS_WELCOME_REPO welcome + chown -R www-data:www-data welcome + fi + popd + + echo "enabled" > /"${BASE_BOOT_MOUNT_PATH}"/check_for_httpd +else + echo "disabled" > /"${BASE_BOOT_MOUNT_PATH}"/check_for_httpd +fi + + +#override timezone +if [ "$FULLPAGEOS_OVERRIDE_TIMEZONE" != "default" ] +then + ln -fs /usr/share/zoneinfo/"$FULLPAGEOS_OVERRIDE_TIMEZONE" /etc/localtime + dpkg-reconfigure -f noninteractive tzdata +fi + +#override locale +if [ "$FULLPAGEOS_OVERRIDE_LOCALE" != "default" ] +then + sed -i '/^#.* '"$FULLPAGEOS_OVERRIDE_LOCALE"' /s/^# //' /etc/locale.gen + locale-gen + update-locale LANG="$FULLPAGEOS_OVERRIDE_LOCALE" +fi + +#override keyboard model and layout +if [ "$FULLPAGEOS_OVERRIDE_KBD_MODEL" != "default" ] +then + sed -i 's/^XKBMODEL=.*/XKBMODEL="'$FULLPAGEOS_OVERRIDE_KBD_MODEL'"/' /etc/default/keyboard +fi +if [ "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" != "default" ] +then + sed -i 's/^XKBLAYOUT=.*/XKBLAYOUT="'$FULLPAGEOS_OVERRIDE_KBD_LAYOUT'"/' /etc/default/keyboard +fi +if [ "$FULLPAGEOS_OVERRIDE_KBD_MODEL" != "default" ] || [ "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" != "default" ] +then + setupcon -k --save-only +fi + +# Add emoji support +sudo -u pi mkdir -p /home/pi/.fonts +sudo -u pi wget --directory-prefix /home/pi/.fonts https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf + +#override password +if [ "$FULLPAGEOS_OVERRIDE_PASSWORD" != "default" ] +then + #root password + echo "pi:$FULLPAGEOS_OVERRIDE_PASSWORD" | chpasswd +fi + +#Setup x11vnc +if [ "$FULLPAGEOS_INCLUDE_X11VNC" == "yes" ] +then + apt-get install -y --force-yes x11vnc + + mkdir -p /opt/custompios/vnc + chown "${BASE_USER}":"${BASE_USER}" /opt/custompios/vnc + + # Set x11vnc password + if [ "$FULLPAGEOS_OVERRIDE_PASSWORD" != "default" ] + then + sudo -u pi /opt/custompios/scripts/setX11vncPass "$FULLPAGEOS_OVERRIDE_PASSWORD" + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created. Trying again." + sudo -u pi /opt/custompios/scripts/setX11vncPass "$FULLPAGEOS_OVERRIDE_PASSWORD" + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created again. Giving up." + echo "Failed to set a VNC password. Aborting build." + exit 1 + fi + fi + else + sudo -u pi /opt/custompios/scripts/setX11vncPass raspberry + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created. Trying again." + sudo -u pi /opt/custompios/scripts/setX11vncPass raspberry + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created again. Giving up." + echo "Failed to set a VNC password. Aborting build." + exit 1 + fi + fi + fi + ls -l /opt/custompios/vnc/passwd + + #Enable x11vnc service + systemctl enable x11vnc.service +fi + +#echo "sudo -u pi startx /opt/custompios/scripts/run_onepageos &" >> /etc/rc.local +#echo "(sleep 15 ; sudo -u pi /opt/custompios/scripts/fullscreen) &" >> /etc/rc.local + +##################################################################### +### setup services + +echo "server time.nist.gov" >> /etc/ntp.conf +echo "server ntp.ubuntu.com" >> /etc/ntp.conf + +#cleanup +apt-get clean +apt-get autoremove -y diff --git a/FullPageOS/src/vagrant/Vagrantfile b/FullPageOS/src/vagrant/Vagrantfile new file mode 100644 index 0000000..a4567d9 --- /dev/null +++ b/FullPageOS/src/vagrant/Vagrantfile @@ -0,0 +1,17 @@ +vagrant_root = File.dirname(__FILE__) +Vagrant.configure("2") do |o| + # o.vm.box = "octopi-build" + o.vm.box= "debian/stretch64" + o.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'" + o.vm.synced_folder File.read("../custompios_path").gsub("\n",""), "/CustomPiOS", create:true, type: "nfs" + o.vm.synced_folder "../", "/distro", create:true, type: "nfs" + o.vm.network :private_network, ip: "192.168.55.55" + o.vm.provision :shell, :path => "setup.sh", args: ENV['SHELL_ARGS'] + + #o.vbguest.auto_update = false + + o.vm.provider "virtualbox" do |v| + v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] + v.customize ["modifyvm", :id, "--natdnsproxy1", "on"] + end +end diff --git a/FullPageOS/src/vagrant/instructions.rst b/FullPageOS/src/vagrant/instructions.rst new file mode 100644 index 0000000..cffe6cd --- /dev/null +++ b/FullPageOS/src/vagrant/instructions.rst @@ -0,0 +1,9 @@ +How to use vagrant image build system +===================================== + +Make sure you have all the requirements +:: + sudo apt-get install vagrant nfs-kernel-server + sudo vagrant plugin install vagrant-nfs_guest + sudo modprobe nfs + sudo vagrant up diff --git a/FullPageOS/src/vagrant/run_vagrant_build.sh b/FullPageOS/src/vagrant/run_vagrant_build.sh new file mode 100755 index 0000000..1ef3189 --- /dev/null +++ b/FullPageOS/src/vagrant/run_vagrant_build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +sudo vagrant ssh -- -t "sudo /CustomPiOS/nightly_build_scripts/custompios_nightly_build $@" + diff --git a/FullPageOS/src/vagrant/setup.sh b/FullPageOS/src/vagrant/setup.sh new file mode 100644 index 0000000..71a32b0 --- /dev/null +++ b/FullPageOS/src/vagrant/setup.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +sudo apt-get update +sudo apt-get install -y gawk util-linux realpath git qemu-user-static p7zip-full unzip zip + diff --git a/FullPageOS/src/variants/no-acceleration/config b/FullPageOS/src/variants/no-acceleration/config new file mode 100755 index 0000000..2585bd4 --- /dev/null +++ b/FullPageOS/src/variants/no-acceleration/config @@ -0,0 +1 @@ +GUI_INCLUDE_ACCELERATION=no From 11a0586e86addd31a1d43c3468ba9ec870627846 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:03:26 -0400 Subject: [PATCH 23/56] copy rpi-deploy folder to FullPageOS filesystem --- FullPageOS/src/modules/rpi-deploy/config | 41 ++++ .../filesystem/home/pi/updatecli}/Dockerfile | 0 .../home/pi/updatecli}/docker-compose.yml | 0 .../new_stuff/updatecli.d/github_release.yml | 0 .../new_stuff/updatecli.d/local_file.yml | 0 .../updatecli.d/scripts/check_esbuild.js | 0 .../scripts/common/create_and_run_service.sh | 0 .../updatecli.d/scripts/compare_with_file.sh | 0 .../updatecli.d/scripts/run_from_esbuild.sh | 0 .../scripts/run_from_github_release.sh | 0 .../services/check_esbuild.service | 0 .../services/check_github_releases.service | 0 .../services/check_github_releases.timer | 0 .../updatecli.d/updatecli_github_commit.yml | 0 .../filesystem/home/pi/updatecli}/pi-init.sh | 0 .../pi/updatecli/systemd/install_updatecli.sh | 48 +++++ .../modules/rpi-deploy/start_chroot_script | 175 ++++++++++++++++++ 17 files changed, 264 insertions(+) create mode 100644 FullPageOS/src/modules/rpi-deploy/config rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/Dockerfile (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/docker-compose.yml (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/github_release.yml (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/local_file.yml (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/scripts/check_esbuild.js (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/scripts/compare_with_file.sh (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/scripts/run_from_esbuild.sh (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/scripts/run_from_github_release.sh (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/services/check_esbuild.service (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/services/check_github_releases.service (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/services/check_github_releases.timer (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/new_stuff/updatecli.d/updatecli_github_commit.yml (100%) rename {updatecli => FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli}/pi-init.sh (100%) create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh create mode 100755 FullPageOS/src/modules/rpi-deploy/start_chroot_script diff --git a/FullPageOS/src/modules/rpi-deploy/config b/FullPageOS/src/modules/rpi-deploy/config new file mode 100644 index 0000000..7458458 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/config @@ -0,0 +1,41 @@ +############################################################################### +#override timezone, otherwise use image timezone +[ -n "$FULLPAGEOS_OVERRIDE_TIMEZONE" ] || FULLPAGEOS_OVERRIDE_TIMEZONE=default + +#override locale, otherwise use image locale +# Run locale -a to get a list of the locale names suitable for use in environment variables. Note that the spellings are different from the ones presented in the dpkg-reconfigure list. +# More information at https://wiki.debian.org/Locale#Standard +[ -n "$FULLPAGEOS_OVERRIDE_LOCALE" ] || FULLPAGEOS_OVERRIDE_LOCALE=default + +#override keyboard model and layout, otherwise use image default +# see `man keyboard` for more information and the file +# `/usr/share/X11/xkb/rules/xorg.lst` for the list of models and layouts +[ -n "$FULLPAGEOS_OVERRIDE_KBD_MODEL" ] || FULLPAGEOS_OVERRIDE_KBD_MODEL=default +[ -n "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" ] || FULLPAGEOS_OVERRIDE_KBD_LAYOUT=default + +#override password, otherwise use image default +[ -n "$FULLPAGEOS_OVERRIDE_PASSWORD" ] || FULLPAGEOS_OVERRIDE_PASSWORD=default + +[ -n "$FULLPAGEOS_INCLUDE_CHROMIUM" ] || FULLPAGEOS_INCLUDE_CHROMIUM=yes +[ -n "$FULLPAGEOS_INCLUDE_LIGHTTPD" ] || FULLPAGEOS_INCLUDE_LIGHTTPD=yes + +# FullPageDashboard repo & branch +[ -n "$FULLPAGEOS_DASHBOARD_REPO_SHIP" ] || FULLPAGEOS_DASHBOARD_REPO_SHIP=https://github.com/amitdar/FullPageDashboard.git +[ -n "$FULLPAGEOS_DASHBOARD_REPO_BUILD" ] || FULLPAGEOS_DASHBOARD_REPO_BUILD= +[ -n "$FULLPAGEOS_DASHBOARD_REPO_BRANCH" ] || FULLPAGEOS_DASHBOARD_REPO_BRANCH=master +[ -n "$FULLPAGEOS_INCLUDE_DASHBOARD" ] || FULLPAGEOS_INCLUDE_DASHBOARD=yes + +# FullPageDashboard repo & branch +[ -n "$FULLPAGEOS_WELCOME_REPO_SHIP" ] || FULLPAGEOS_WELCOME_REPO_SHIP=https://github.com/tailorvj/FullPageOSWelcome.git +[ -n "$FULLPAGEOS_WELCOME_REPO_BUILD" ] || FULLPAGEOS_WELCOME_REPO_BUILD= +[ -n "$FULLPAGEOS_WELCOME_REPO_BRANCH" ] || FULLPAGEOS_WELCOME_REPO_BRANCH=master +[ -n "$FULLPAGEOS_INCLUDE_WELCOME" ] || FULLPAGEOS_INCLUDE_WELCOME=yes + +# Add GPU acceleration +[ -n "$FULLPAGEOS_INCLUDE_ACCELERATION" ] || FULLPAGEOS_INCLUDE_ACCELERATION=yes + +# Enable custom Splashscreen. Put your picture into FullPageOS/modules/fullpageos/filesystem/home/pi/media/splash.png +[ -n "$FULLPAGEOS_CUSTOM_SPLASHSCREEN" ] || FULLPAGEOS_CUSTOM_SPLASHSCREEN=yes + +# Install and enable x11vnc service +[ -n "$FULLPAGEOS_INCLUDE_X11VNC" ] || FULLPAGEOS_INCLUDE_X11VNC=yes diff --git a/updatecli/Dockerfile b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/Dockerfile similarity index 100% rename from updatecli/Dockerfile rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/Dockerfile diff --git a/updatecli/docker-compose.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/docker-compose.yml similarity index 100% rename from updatecli/docker-compose.yml rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/docker-compose.yml diff --git a/updatecli/new_stuff/updatecli.d/github_release.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/github_release.yml similarity index 100% rename from updatecli/new_stuff/updatecli.d/github_release.yml rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/github_release.yml diff --git a/updatecli/new_stuff/updatecli.d/local_file.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml similarity index 100% rename from updatecli/new_stuff/updatecli.d/local_file.yml rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml diff --git a/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js similarity index 100% rename from updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js diff --git a/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh similarity index 100% rename from updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh diff --git a/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh similarity index 100% rename from updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh similarity index 100% rename from updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh diff --git a/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh similarity index 100% rename from updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh diff --git a/updatecli/new_stuff/updatecli.d/services/check_esbuild.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service similarity index 100% rename from updatecli/new_stuff/updatecli.d/services/check_esbuild.service rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service diff --git a/updatecli/new_stuff/updatecli.d/services/check_github_releases.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service similarity index 100% rename from updatecli/new_stuff/updatecli.d/services/check_github_releases.service rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service diff --git a/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer similarity index 100% rename from updatecli/new_stuff/updatecli.d/services/check_github_releases.timer rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer diff --git a/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml similarity index 100% rename from updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml diff --git a/updatecli/pi-init.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh similarity index 100% rename from updatecli/pi-init.sh rename to FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh new file mode 100644 index 0000000..6233b13 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Define the desired version of updatecli +UPDATECLI_VERSION="v0.93.0" +ARCH=$(uname -m) + +# Determine the architecture +case $ARCH in + x86_64) + ARCH="amd64" + ;; + aarch64) + ARCH="arm64" + ;; + armv6l) + ARCH="armv6" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +# Installation directory +INSTALL_DIR="/usr/local/bin" + +# Check if updatecli is already installed and at the desired version +if command -v updatecli &>/dev/null; then + INSTALLED_VERSION=$(updatecli version | awk '{print $3}') + if [ "$INSTALLED_VERSION" == "$UPDATECLI_VERSION" ]; then + echo "updatecli $UPDATECLI_VERSION is already installed." + exit 0 + else + echo "Updating updatecli from version $INSTALLED_VERSION to $UPDATECLI_VERSION." + fi +else + echo "Installing updatecli $UPDATECLI_VERSION." +fi + +# Download and install updatecli +TMP_DIR=$(mktemp -d) +curl -sL -o "$TMP_DIR/updatecli_${ARCH}.tgz" "https://github.com/updatecli/updatecli/releases/download/$UPDATECLI_VERSION/updatecli_${ARCH}.tgz" +tar -xzf "$TMP_DIR/updatecli_${ARCH}.tgz" -C "$TMP_DIR" +mv "$TMP_DIR/updatecli" "$INSTALL_DIR/updatecli" +chmod +x "$INSTALL_DIR/updatecli" +rm -rf "$TMP_DIR" + +echo "updatecli $UPDATECLI_VERSION has been installed successfully." diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script new file mode 100755 index 0000000..ce960f8 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# FullPageOS generation script +# Helper script that runs in a Raspbian/others chroot to create the FullPageOS distro +# Written by Guy Sheffer +# GPL V3 +######## +set -x +set -e + +source /common.sh + +unpack /filesystem/opt /opt +unpack /filesystem/boot /"${BASE_BOOT_MOUNT_PATH}" +unpack /filesystem/root_init / + +apt-get update + +# Display a custom Splashscreen when booting the Rpi +if [ "$FULLPAGEOS_CUSTOM_SPLASHSCREEN" == "yes" ] +then + apt-get install -y fbi + if [ "${BASE_BOARD}" == raspberrypi* ]; then + sed -i 's/$/ logo.nologo consoleblank=0 loglevel=0 quiet/' /"${BASE_BOOT_MOUNT_PATH}"/cmdline.txt + fi + echo "disable_splash=1" >> /"${BASE_BOOT_MOUNT_PATH}"/config.txt + systemctl enable splashscreen.service + systemctl disable getty@tty1 +fi + +remove_extra=$(remove_if_installed scratch squeak-plugins-scratch squeak-vm python-minecraftpi minecraft-pi sonic-pi oracle-java8-jdk bluej greenfoot libreoffice-common libreoffice-core freepats) + +apt-get remove -y --purge $remove_extra + +apt-get autoremove -y + +#apt-get tools +apt-get -y --force-yes install git screen checkinstall avahi-daemon libavahi-compat-libdnssd1 xterm xdotool vim expect feh pulseaudio + +if [ "$FULLPAGEOS_INCLUDE_CHROMIUM" == "yes" ] +then + apt-get install -y --force-yes chromium-browser + sed -i 's@%BROWSER_START_SCRIPT%@/opt/custompios/scripts/start_chromium_browser@g' /opt/custompios/scripts/run_onepageos +fi + +#Install web stack +if [ "$FULLPAGEOS_INCLUDE_LIGHTTPD" == "yes" ] +then + apt-get install -y lighttpd php-common php-cgi php + lighty-enable-mod fastcgi-php + #service lighttpd force-reload + chown -R www-data:www-data /var/www/html + chmod 775 /var/www/html + usermod -a -G www-data pi + systemctl enable clear_lighttpd_cache.service + pushd /var/www/html + #Put git clones in place + if [ "$FULLPAGEOS_INCLUDE_DASHBOARD" == "yes" ] + then + gitclone FULLPAGEOS_DASHBOARD_REPO FullPageDashboard + chown -R pi:pi FullPageDashboard + chown -R www-data:www-data FullPageDashboard + chmod 775 FullPageDashboard + pushd FullPageDashboard + sed -i "s@'INIT_URL_PATH', __DIR__ . '/init.txt'@'INIT_URL_PATH', '/"${BASE_BOOT_MOUNT_PATH}"/fullpagedashboard.txt'@g" config.php + popd + fi + #Set Welcome screen + if [ "$FULLPAGEOS_INCLUDE_WELCOME" == "yes" ] + then + gitclone FULLPAGEOS_WELCOME_REPO welcome + chown -R www-data:www-data welcome + fi + popd + + echo "enabled" > /"${BASE_BOOT_MOUNT_PATH}"/check_for_httpd +else + echo "disabled" > /"${BASE_BOOT_MOUNT_PATH}"/check_for_httpd +fi + + +#override timezone +if [ "$FULLPAGEOS_OVERRIDE_TIMEZONE" != "default" ] +then + ln -fs /usr/share/zoneinfo/"$FULLPAGEOS_OVERRIDE_TIMEZONE" /etc/localtime + dpkg-reconfigure -f noninteractive tzdata +fi + +#override locale +if [ "$FULLPAGEOS_OVERRIDE_LOCALE" != "default" ] +then + sed -i '/^#.* '"$FULLPAGEOS_OVERRIDE_LOCALE"' /s/^# //' /etc/locale.gen + locale-gen + update-locale LANG="$FULLPAGEOS_OVERRIDE_LOCALE" +fi + +#override keyboard model and layout +if [ "$FULLPAGEOS_OVERRIDE_KBD_MODEL" != "default" ] +then + sed -i 's/^XKBMODEL=.*/XKBMODEL="'$FULLPAGEOS_OVERRIDE_KBD_MODEL'"/' /etc/default/keyboard +fi +if [ "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" != "default" ] +then + sed -i 's/^XKBLAYOUT=.*/XKBLAYOUT="'$FULLPAGEOS_OVERRIDE_KBD_LAYOUT'"/' /etc/default/keyboard +fi +if [ "$FULLPAGEOS_OVERRIDE_KBD_MODEL" != "default" ] || [ "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" != "default" ] +then + setupcon -k --save-only +fi + +# Add emoji support +sudo -u pi mkdir -p /home/pi/.fonts +sudo -u pi wget --directory-prefix /home/pi/.fonts https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf + +#override password +if [ "$FULLPAGEOS_OVERRIDE_PASSWORD" != "default" ] +then + #root password + echo "pi:$FULLPAGEOS_OVERRIDE_PASSWORD" | chpasswd +fi + +#Setup x11vnc +if [ "$FULLPAGEOS_INCLUDE_X11VNC" == "yes" ] +then + apt-get install -y --force-yes x11vnc + + mkdir -p /opt/custompios/vnc + chown "${BASE_USER}":"${BASE_USER}" /opt/custompios/vnc + + # Set x11vnc password + if [ "$FULLPAGEOS_OVERRIDE_PASSWORD" != "default" ] + then + sudo -u pi /opt/custompios/scripts/setX11vncPass "$FULLPAGEOS_OVERRIDE_PASSWORD" + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created. Trying again." + sudo -u pi /opt/custompios/scripts/setX11vncPass "$FULLPAGEOS_OVERRIDE_PASSWORD" + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created again. Giving up." + echo "Failed to set a VNC password. Aborting build." + exit 1 + fi + fi + else + sudo -u pi /opt/custompios/scripts/setX11vncPass raspberry + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created. Trying again." + sudo -u pi /opt/custompios/scripts/setX11vncPass raspberry + sync + if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then + echo "/opt/custompios/vnc/passwd was not created again. Giving up." + echo "Failed to set a VNC password. Aborting build." + exit 1 + fi + fi + fi + ls -l /opt/custompios/vnc/passwd + + #Enable x11vnc service + systemctl enable x11vnc.service +fi + +#echo "sudo -u pi startx /opt/custompios/scripts/run_onepageos &" >> /etc/rc.local +#echo "(sleep 15 ; sudo -u pi /opt/custompios/scripts/fullscreen) &" >> /etc/rc.local + +##################################################################### +### setup services + +echo "server time.nist.gov" >> /etc/ntp.conf +echo "server ntp.ubuntu.com" >> /etc/ntp.conf + +#cleanup +apt-get clean +apt-get autoremove -y From 0a77a94269de35097a0197f6a59248f4f6182faf Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:14:25 -0400 Subject: [PATCH 24/56] change start_chroot_script to be more rpi-deploy focused --- .../modules/rpi-deploy/start_chroot_script | 163 ++---------------- 1 file changed, 11 insertions(+), 152 deletions(-) diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index ce960f8..2342443 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -9,163 +9,22 @@ set -e source /common.sh -unpack /filesystem/opt /opt -unpack /filesystem/boot /"${BASE_BOOT_MOUNT_PATH}" -unpack /filesystem/root_init / +unpack /filesystem/home /home +# unpack /filesystem/opt /opt +# unpack /filesystem/boot /"${BASE_BOOT_MOUNT_PATH}" +# unpack /filesystem/root_init / apt-get update -# Display a custom Splashscreen when booting the Rpi -if [ "$FULLPAGEOS_CUSTOM_SPLASHSCREEN" == "yes" ] -then - apt-get install -y fbi - if [ "${BASE_BOARD}" == raspberrypi* ]; then - sed -i 's/$/ logo.nologo consoleblank=0 loglevel=0 quiet/' /"${BASE_BOOT_MOUNT_PATH}"/cmdline.txt - fi - echo "disable_splash=1" >> /"${BASE_BOOT_MOUNT_PATH}"/config.txt - systemctl enable splashscreen.service - systemctl disable getty@tty1 -fi +sudo apt install -y cockpit +sudo apt install -y npm -remove_extra=$(remove_if_installed scratch squeak-plugins-scratch squeak-vm python-minecraftpi minecraft-pi sonic-pi oracle-java8-jdk bluej greenfoot libreoffice-common libreoffice-core freepats) +curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_arm64.deb && \ + sudo dpkg -i /tmp/updatecli_arm64.deb && \ + rm /tmp/updatecli_arm64.deb -apt-get remove -y --purge $remove_extra - -apt-get autoremove -y - -#apt-get tools -apt-get -y --force-yes install git screen checkinstall avahi-daemon libavahi-compat-libdnssd1 xterm xdotool vim expect feh pulseaudio - -if [ "$FULLPAGEOS_INCLUDE_CHROMIUM" == "yes" ] -then - apt-get install -y --force-yes chromium-browser - sed -i 's@%BROWSER_START_SCRIPT%@/opt/custompios/scripts/start_chromium_browser@g' /opt/custompios/scripts/run_onepageos -fi - -#Install web stack -if [ "$FULLPAGEOS_INCLUDE_LIGHTTPD" == "yes" ] -then - apt-get install -y lighttpd php-common php-cgi php - lighty-enable-mod fastcgi-php - #service lighttpd force-reload - chown -R www-data:www-data /var/www/html - chmod 775 /var/www/html - usermod -a -G www-data pi - systemctl enable clear_lighttpd_cache.service - pushd /var/www/html - #Put git clones in place - if [ "$FULLPAGEOS_INCLUDE_DASHBOARD" == "yes" ] - then - gitclone FULLPAGEOS_DASHBOARD_REPO FullPageDashboard - chown -R pi:pi FullPageDashboard - chown -R www-data:www-data FullPageDashboard - chmod 775 FullPageDashboard - pushd FullPageDashboard - sed -i "s@'INIT_URL_PATH', __DIR__ . '/init.txt'@'INIT_URL_PATH', '/"${BASE_BOOT_MOUNT_PATH}"/fullpagedashboard.txt'@g" config.php - popd - fi - #Set Welcome screen - if [ "$FULLPAGEOS_INCLUDE_WELCOME" == "yes" ] - then - gitclone FULLPAGEOS_WELCOME_REPO welcome - chown -R www-data:www-data welcome - fi - popd - - echo "enabled" > /"${BASE_BOOT_MOUNT_PATH}"/check_for_httpd -else - echo "disabled" > /"${BASE_BOOT_MOUNT_PATH}"/check_for_httpd -fi - - -#override timezone -if [ "$FULLPAGEOS_OVERRIDE_TIMEZONE" != "default" ] -then - ln -fs /usr/share/zoneinfo/"$FULLPAGEOS_OVERRIDE_TIMEZONE" /etc/localtime - dpkg-reconfigure -f noninteractive tzdata -fi - -#override locale -if [ "$FULLPAGEOS_OVERRIDE_LOCALE" != "default" ] -then - sed -i '/^#.* '"$FULLPAGEOS_OVERRIDE_LOCALE"' /s/^# //' /etc/locale.gen - locale-gen - update-locale LANG="$FULLPAGEOS_OVERRIDE_LOCALE" -fi - -#override keyboard model and layout -if [ "$FULLPAGEOS_OVERRIDE_KBD_MODEL" != "default" ] -then - sed -i 's/^XKBMODEL=.*/XKBMODEL="'$FULLPAGEOS_OVERRIDE_KBD_MODEL'"/' /etc/default/keyboard -fi -if [ "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" != "default" ] -then - sed -i 's/^XKBLAYOUT=.*/XKBLAYOUT="'$FULLPAGEOS_OVERRIDE_KBD_LAYOUT'"/' /etc/default/keyboard -fi -if [ "$FULLPAGEOS_OVERRIDE_KBD_MODEL" != "default" ] || [ "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" != "default" ] -then - setupcon -k --save-only -fi - -# Add emoji support -sudo -u pi mkdir -p /home/pi/.fonts -sudo -u pi wget --directory-prefix /home/pi/.fonts https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf - -#override password -if [ "$FULLPAGEOS_OVERRIDE_PASSWORD" != "default" ] -then - #root password - echo "pi:$FULLPAGEOS_OVERRIDE_PASSWORD" | chpasswd -fi - -#Setup x11vnc -if [ "$FULLPAGEOS_INCLUDE_X11VNC" == "yes" ] -then - apt-get install -y --force-yes x11vnc - - mkdir -p /opt/custompios/vnc - chown "${BASE_USER}":"${BASE_USER}" /opt/custompios/vnc - - # Set x11vnc password - if [ "$FULLPAGEOS_OVERRIDE_PASSWORD" != "default" ] - then - sudo -u pi /opt/custompios/scripts/setX11vncPass "$FULLPAGEOS_OVERRIDE_PASSWORD" - sync - if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then - echo "/opt/custompios/vnc/passwd was not created. Trying again." - sudo -u pi /opt/custompios/scripts/setX11vncPass "$FULLPAGEOS_OVERRIDE_PASSWORD" - sync - if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then - echo "/opt/custompios/vnc/passwd was not created again. Giving up." - echo "Failed to set a VNC password. Aborting build." - exit 1 - fi - fi - else - sudo -u pi /opt/custompios/scripts/setX11vncPass raspberry - sync - if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then - echo "/opt/custompios/vnc/passwd was not created. Trying again." - sudo -u pi /opt/custompios/scripts/setX11vncPass raspberry - sync - if [ ! -f /opt/custompios/vnc/passwd ] || [ ! -s /opt/custompios/vnc/passwd ]; then - echo "/opt/custompios/vnc/passwd was not created again. Giving up." - echo "Failed to set a VNC password. Aborting build." - exit 1 - fi - fi - fi - ls -l /opt/custompios/vnc/passwd - - #Enable x11vnc service - systemctl enable x11vnc.service -fi - -#echo "sudo -u pi startx /opt/custompios/scripts/run_onepageos &" >> /etc/rc.local -#echo "(sleep 15 ; sudo -u pi /opt/custompios/scripts/fullscreen) &" >> /etc/rc.local - -##################################################################### -### setup services +npm init -y +npm i eventsource echo "server time.nist.gov" >> /etc/ntp.conf echo "server ntp.ubuntu.com" >> /etc/ntp.conf From b9de19e8cc3e17a66d4214faac67bec358b675e2 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:18:24 -0400 Subject: [PATCH 25/56] replace ci workflow with custompios --- .github/workflows/build_rpi.yml | 67 --------------------- .github/workflows/build_rpi_chroot.yml | 30 --------- .github/workflows/build_with_custompios.yml | 66 ++++++++++++++++++++ 3 files changed, 66 insertions(+), 97 deletions(-) delete mode 100644 .github/workflows/build_rpi.yml delete mode 100644 .github/workflows/build_rpi_chroot.yml create mode 100644 .github/workflows/build_with_custompios.yml diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml deleted file mode 100644 index d230592..0000000 --- a/.github/workflows/build_rpi.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Dispatched Build and publish RaspAP images - -permissions: - contents: write - -on: - release: - types: [published] - workflow_dispatch: - pull_request: - -jobs: - build-raspap-image: - runs-on: ubuntu-latest - strategy: - matrix: - include: - - arch: "32-bit" - pi_gen_version: "master" - - arch: "64-bit" - pi_gen_version: "arm64" - fail-fast: false - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # - name: Add RaspAP Stage - # run: | - # mkdir -p stage-raspap/package-raspap && - # { - # cat > stage-raspap/package-raspap/00-run-chroot.sh <<-EOF - # #!/bin/bash - # apt-get update -y && apt-get install -y curl dhcpcd5 iptables procps - # curl -sL https://install.raspap.com | bash -s -- --yes --openvpn 1 --restapi 1 --adblock 1 --wireguard 1 --tcp-bbr 1 --check 0 - - # # Set Wi-Fi country to prevent RF kill - # raspi-config nonint do_wifi_country "US" - # EOF - # } && - # chmod +x stage-raspap/package-raspap/00-run-chroot.sh && - # { - # cat > stage-raspap/prerun.sh <<-EOF - # #!/bin/bash -e - # if [ ! -d "\${ROOTFS_DIR}" ]; then - # copy_previous - # fi - # EOF - # } && - # chmod +x stage-raspap/prerun.sh - - - name: Build RaspAP Image - id: build - uses: usimd/pi-gen-action@v1 - with: - image-name: "raspap-bookworm-${{ matrix.arch == '32-bit' && 'armhf' || 'arm64' }}-lite-${{ github.event.inputs.tag || github.ref_name }}" - enable-ssh: 1 - stage-list: stage0 stage1 stage2 - # stage-list: stage0 stage1 stage2 ./stage-raspap - verbose-output: true - pi-gen-version: ${{ matrix.pi_gen_version }} - pi-gen-repository: RaspAP/pi-gen - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: rpi-deploy-${{ matrix.arch }}.img.zip - path: ${{ steps.build.outputs.image-path }} diff --git a/.github/workflows/build_rpi_chroot.yml b/.github/workflows/build_rpi_chroot.yml deleted file mode 100644 index c3f54ea..0000000 --- a/.github/workflows/build_rpi_chroot.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Build Raspberry Pi image with chroot - -on: - push: - branches: - - "*" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: pguyot/arm-runner-action@v2 - id: build_image - with: - commands: | - echo yay - - - name: Compress the release image - # if: github.ref == 'refs/heads/releng' || startsWith(github.ref, 'refs/tags/') - run: | - mv ${{ steps.build_image.outputs.image }} my-release-image.img - xz -0 -T 0 -v my-release-image.img - - - name: Upload release image - uses: actions/upload-artifact@v4 - # if: github.ref == 'refs/heads/releng' || startsWith(github.ref, 'refs/tags/') - with: - name: Release image - path: my-release-image.img.xz diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml new file mode 100644 index 0000000..9796fcd --- /dev/null +++ b/.github/workflows/build_with_custompios.yml @@ -0,0 +1,66 @@ +name: Build Image + +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Free up disk space + uses: insightsengineering/disk-space-reclaimer@v1 + with: + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: true + docker-images: true + tools-cache: false + + - name: Check disk space + run: df -h + - name: Update apt + run: sudo apt-get update + - name: Install Dependencies + run: sudo apt install coreutils p7zip-full qemu-user-static python3-git + - name: Checkout CustomPiOS + uses: actions/checkout@v2 + with: + repository: 'guysoft/CustomPiOS' + path: CustomPiOS + - name: Checkout Project Repository + uses: actions/checkout@v2 + with: + repository: ${{ github.repository }} + path: repository_temp + submodules: true + - name: Move FullPageOS folder + run: | + mv repository_temp/FullPageOS repository + # - name: Setup Deno + # uses: denoland/setup-deno@v2 + # with: + # deno-version: v2.x + # - name: Compile IP Configurator + # run: cd repository/apps/ip_configurator && deno task compile + # - name: Move IP Configurator Executable + # run: | + # cp repository/apps/ip_configurator/ip_configurator_executable repository/src/modules/fullpageos/filesystem/home/pi/apps/ + # chmod +x repository/src/modules/fullpageos/filesystem/home/pi/apps/ip_configurator_executable + - name: Download Raspbian Image + run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_arm64_latest' + - name: Update CustomPiOS Paths + run: cd repository/src && ../../CustomPiOS/src/update-custompios-paths + - name: Build Image + run: sudo modprobe loop && cd repository/src && sudo BASE_ARCH=arm64 bash -x ./build_dist + - name: Copy Output + run: cp ${{ github.workspace }}/repository/src/workspace/*-raspios-*-lite.img build.img + - name: Zip Output + run: gzip build.img + - name: Set artifact name + id: artifact + run: echo "name=$(date '+%Y%m%d_%H%M%S')_${{ github.sha }}_toucheth.img.gz" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact.outputs.name }} + path: build.img.gz From 11442772bee77d68394fe1fa7a1e158f793863cf Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:31:37 -0400 Subject: [PATCH 26/56] change image architecture --- .github/workflows/build_with_custompios.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index 9796fcd..4f70c01 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -48,11 +48,11 @@ jobs: # cp repository/apps/ip_configurator/ip_configurator_executable repository/src/modules/fullpageos/filesystem/home/pi/apps/ # chmod +x repository/src/modules/fullpageos/filesystem/home/pi/apps/ip_configurator_executable - name: Download Raspbian Image - run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_arm64_latest' + run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' - name: Update CustomPiOS Paths run: cd repository/src && ../../CustomPiOS/src/update-custompios-paths - name: Build Image - run: sudo modprobe loop && cd repository/src && sudo BASE_ARCH=arm64 bash -x ./build_dist + run: sudo modprobe loop && cd repository/src && sudo BASE_ARCH=armhf bash -x ./build_dist - name: Copy Output run: cp ${{ github.workspace }}/repository/src/workspace/*-raspios-*-lite.img build.img - name: Zip Output From 0b2638459ddc8720eb684de6ec91915fc565c152 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:16:32 -0400 Subject: [PATCH 27/56] change /home/jamtools to /home/pi --- .../updatecli/new_stuff/updatecli.d/local_file.yml | 4 ++-- .../new_stuff/updatecli.d/scripts/check_esbuild.js | 4 ++-- .../scripts/common/create_and_run_service.sh | 2 +- .../updatecli.d/scripts/run_from_esbuild.sh | 4 ++-- .../updatecli.d/scripts/run_from_github_release.sh | 12 ++++++------ .../updatecli.d/services/check_esbuild.service | 2 +- .../services/check_github_releases.service | 4 ++-- .../updatecli.d/updatecli_github_commit.yml | 4 ++-- .../filesystem/home/pi/updatecli/pi-init.sh | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) mode change 100644 => 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml index d964ea9..bec3c43 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml @@ -10,9 +10,9 @@ conditions: kind: shell sourceid: fileHash spec: - command: /home/jamtools/code/scripts/compare_with_file.sh ./.last_index_hash + command: /home/pi/code/scripts/compare_with_file.sh ./.last_index_hash targets: fetchBinary: kind: shell spec: - command: /home/jamtools/code/scripts/run_from_esbuild.sh myapp + command: /home/pi/code/scripts/run_from_esbuild.sh myapp diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js index 3e682a6..db49e3b 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js @@ -11,8 +11,8 @@ setTimeout(async () => { }); const run = () => { - spawn('/home/jamtools/code/scripts/run_from_esbuild.sh', ['myapp'], { - // spawn('updatecli', ['apply', '--config', '/home/jamtools/code/local_file.yml'], { + spawn('/home/pi/code/scripts/run_from_esbuild.sh', ['myapp'], { + // spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { env: { PATH: process.env.PATH, }, diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh index ce91ff0..4e33eaa 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh @@ -17,7 +17,7 @@ After=network.target Type=simple ExecStart=$COMMAND Restart=on-failure -WorkingDirectory=/home/jamtools/code/artifacts +WorkingDirectory=/home/pi/code/artifacts [Install] WantedBy=multi-user.target" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh index 9551351..2f0e0a6 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh @@ -2,6 +2,6 @@ set -e mkdir -p artifacts -curl -sL -o /home/jamtools/code/artifacts/index.js http://jam.local:1380/index.js +curl -sL -o /home/pi/code/artifacts/index.js http://jam.local:1380/index.js -/home/jamtools/code/scripts/common/create_and_run_service.sh $1 "node /home/jamtools/code/artifacts/index.js" +/home/pi/code/scripts/common/create_and_run_service.sh $1 "node /home/pi/code/artifacts/index.js" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh index c3f2906..3a62abc 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh @@ -2,15 +2,15 @@ set -e -mkdir -p /home/jamtools/code/artifacts -curl -sL -o /home/jamtools/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" +mkdir -p /home/pi/code/artifacts +curl -sL -o /home/pi/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" -mkdir -p /home/jamtools/code/artifacts/dist -unzip -o /home/jamtools/code/artifacts/dist.zip -d /home/jamtools/code/artifacts/dist +mkdir -p /home/pi/code/artifacts/dist +unzip -o /home/pi/code/artifacts/dist.zip -d /home/pi/code/artifacts/dist -/home/jamtools/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/jamtools/code/artifacts/dist/server/dist/local-server.cjs" +/home/pi/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" -source /home/jamtools/code/secrets.env +source /home/pi/code/secrets.env if [ -n "${WEBHOOK_URL}" ]; then echo "Sending webhook notification" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service index 47749a2..c9e2922 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service @@ -4,7 +4,7 @@ After=network.target [Service] Restart=always -ExecStart=/usr/bin/node /home/jamtools/code/scripts/check_esbuild.js +ExecStart=/usr/bin/node /home/pi/code/scripts/check_esbuild.js [Install] WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service index 50126b7..b646a5e 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service @@ -4,8 +4,8 @@ After=network.target [Service] Type=oneshot -ExecStart=/usr/bin/updatecli apply --config /home/jamtools/code/updatecli_github_commit.yml -EnvironmentFile=/home/jamtools/code/secrets.env +ExecStart=/usr/bin/updatecli apply --config /home/pi/code/updatecli_github_commit.yml +EnvironmentFile=/home/pi/secrets.env [Install] WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml index c142b28..b1aa0df 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml @@ -16,11 +16,11 @@ conditions: kind: shell sourceid: newRelease spec: - command: /home/jamtools/code/scripts/compare_with_file.sh /home/jamtools/code/.last_release_tag + command: /home/pi/code/scripts/compare_with_file.sh /home/pi/code/.last_release_tag targets: runFromRelease: kind: shell sourceid: newRelease spec: - command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools jamscribe + command: /home/pi/code/scripts/run_from_github_release.sh jamtools jamscribe diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh old mode 100644 new mode 100755 index bd2cdda..91da0ef --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh @@ -34,7 +34,7 @@ setTimeout(async () => { }); const run = () => { - spawn('updatecli', ['apply', '--config', '/home/jamtools/code/local_file.yml'], { + spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { env: { PATH: process.env.PATH, }, From 01b3e873347adb7c532be9725956c9ee06376065 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Sun, 31 Aug 2025 20:22:31 -0400 Subject: [PATCH 28/56] Create claude.yml --- .github/workflows/claude.yml | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..69dcc67 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,63 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) + # model: "claude-opus-4-20250514" + + # Optional: Customize the trigger phrase (default: @claude) + # trigger_phrase: "/claude" + + # Optional: Trigger when specific user is assigned to an issue + # assignee_trigger: "claude-bot" + + # Optional: Allow Claude to run specific commands + # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" + + # Optional: Add custom instructions for Claude to customize its behavior for your project + # custom_instructions: | + # Follow our coding standards + # Ensure all new code has tests + # Use TypeScript for new files + + # Optional: Custom environment variables for Claude + # claude_env: | + # NODE_ENV: test From d613263c5e28e7d47d7d47e47715da1d18bdca8c Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Sun, 31 Aug 2025 21:21:06 -0400 Subject: [PATCH 29/56] Create claude.yml --- .github/workflows/claude.yml | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..69dcc67 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,63 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) + # model: "claude-opus-4-20250514" + + # Optional: Customize the trigger phrase (default: @claude) + # trigger_phrase: "/claude" + + # Optional: Trigger when specific user is assigned to an issue + # assignee_trigger: "claude-bot" + + # Optional: Allow Claude to run specific commands + # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" + + # Optional: Add custom instructions for Claude to customize its behavior for your project + # custom_instructions: | + # Follow our coding standards + # Ensure all new code has tests + # Use TypeScript for new files + + # Optional: Custom environment variables for Claude + # claude_env: | + # NODE_ENV: test From ce2eefa41c45f1ea846a9d32134fcef8a8f84f56 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:23:44 +0000 Subject: [PATCH 30/56] remove claude.yml --- .github/workflows/claude.yml | 63 ------------------------------------ 1 file changed, 63 deletions(-) delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 69dcc67..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Optional: Customize the trigger phrase (default: @claude) - # trigger_phrase: "/claude" - - # Optional: Trigger when specific user is assigned to an issue - # assignee_trigger: "claude-bot" - - # Optional: Allow Claude to run specific commands - # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - - # Optional: Add custom instructions for Claude to customize its behavior for your project - # custom_instructions: | - # Follow our coding standards - # Ensure all new code has tests - # Use TypeScript for new files - - # Optional: Custom environment variables for Claude - # claude_env: | - # NODE_ENV: test From c4370d6cc3f77322bd296790d5a3a4b0bfdaffb3 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:26:12 +0000 Subject: [PATCH 31/56] disable build_rpi.yml by making it only run through workflow_dispatch --- .github/workflows/build_rpi.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build_rpi.yml b/.github/workflows/build_rpi.yml index cd1f99a..e8cda7e 100644 --- a/.github/workflows/build_rpi.yml +++ b/.github/workflows/build_rpi.yml @@ -4,10 +4,7 @@ permissions: contents: write on: - release: - types: [published] workflow_dispatch: - pull_request: jobs: build-raspap-image: From 6bcfbcb21b8ec12e0c6d6eda749573c6fc546adb Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:29:51 +0000 Subject: [PATCH 32/56] Convert build workflow to reusable composite action - Create composite action at .github/actions/build-rpi-image/action.yml - Add configurable source_repo and version_prefix inputs - Implement release download logic with tag prefix matching - Place downloaded zip at correct path for updatecli scripts - Create test workflow that calls action with jamtools/jamscribe and vjamscribe- prefix Co-authored-by: Michael Kochell --- .github/actions/build-rpi-image/action.yml | 125 +++++++++++++++++++++ .github/workflows/test-build-action.yml | 21 ++++ 2 files changed, 146 insertions(+) create mode 100644 .github/actions/build-rpi-image/action.yml create mode 100644 .github/workflows/test-build-action.yml diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml new file mode 100644 index 0000000..7d28173 --- /dev/null +++ b/.github/actions/build-rpi-image/action.yml @@ -0,0 +1,125 @@ +name: 'Build RPi Image with Release Download' +description: 'Downloads latest release from a repo and builds a Raspberry Pi image' +inputs: + source_repo: + description: 'GitHub repository to download release from (e.g., jamtools/jamscribe)' + required: true + version_prefix: + description: 'Version tag prefix to match releases (e.g., vjamscribe-)' + required: true + github_token: + description: 'GitHub token for API access' + required: true + default: ${{ github.token }} + +runs: + using: 'composite' + steps: + - name: Find latest matching release by tag prefix + id: find_release + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + run: | + PREFIX="${{ inputs.version_prefix }}" + latest=$(gh release list --repo "${{ inputs.source_repo }}" --limit 100 --json tagName,publishedAt \ + | jq -r --arg prefix "$PREFIX" ' + map(select(.tagName | startswith($prefix))) + | sort_by(.publishedAt) + | last + | if . == null then "none" else .tagName end + ') + echo "latest_tag=$latest" >> $GITHUB_OUTPUT + echo "Found latest release: $latest" + + - name: Download artifact from latest release + if: steps.find_release.outputs.latest_tag != 'none' + uses: blauqs/actions-download-asset@master + with: + repo: ${{ inputs.source_repo }} + version: ${{ steps.find_release.outputs.latest_tag }} + file: dist.zip + out: previous_release.zip + token: ${{ inputs.github_token }} + + - name: Free up disk space + uses: insightsengineering/disk-space-reclaimer@v1 + with: + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: true + docker-images: true + tools-cache: false + + - name: Check disk space + shell: bash + run: df -h + + - name: Update apt + shell: bash + run: sudo apt-get update + + - name: Install Dependencies + shell: bash + run: sudo apt install coreutils p7zip-full qemu-user-static python3-git + + - name: Checkout CustomPiOS + uses: actions/checkout@v2 + with: + repository: 'guysoft/CustomPiOS' + path: CustomPiOS + + - name: Checkout Project Repository + uses: actions/checkout@v2 + with: + repository: ${{ github.repository }} + path: repository_temp + submodules: true + + - name: Move FullPageOS folder + shell: bash + run: | + mv repository_temp/FullPageOS repository + + - name: Place downloaded release zip + if: steps.find_release.outputs.latest_tag != 'none' + shell: bash + run: | + # Create the directory structure where updatecli expects the zip file on the RPi + mkdir -p repository/src/modules/rpi-deploy/filesystem/home/jamtools/code/artifacts/ + # Move the downloaded zip to where the updatecli scripts expect it + mv previous_release.zip repository/src/modules/rpi-deploy/filesystem/home/jamtools/code/artifacts/dist.zip + echo "Placed release zip at: repository/src/modules/rpi-deploy/filesystem/home/jamtools/code/artifacts/dist.zip" + + - name: Download Raspbian Image + shell: bash + run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' + + - name: Update CustomPiOS Paths + shell: bash + run: cd repository/src && ../../CustomPiOS/src/update-custompios-paths + + - name: Build Image + shell: bash + run: sudo modprobe loop && cd repository/src && sudo BASE_ARCH=armhf bash -x ./build_dist + + - name: Copy Output + shell: bash + run: cp ${{ github.workspace }}/repository/src/workspace/*-raspios-*-lite.img build.img + + - name: Zip Output + shell: bash + run: gzip build.img + + - name: Set artifact name + id: artifact + shell: bash + run: echo "name=$(date '+%Y%m%d_%H%M%S')_${{ github.sha }}_${{ inputs.version_prefix }}rpi.img.gz" >> $GITHUB_OUTPUT + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact.outputs.name }} + path: build.img.gz \ No newline at end of file diff --git a/.github/workflows/test-build-action.yml b/.github/workflows/test-build-action.yml new file mode 100644 index 0000000..fe15d7c --- /dev/null +++ b/.github/workflows/test-build-action.yml @@ -0,0 +1,21 @@ +name: Test RPi Image Build Action + +on: + workflow_dispatch: + push: + paths: + - '.github/actions/build-rpi-image/**' + +jobs: + test-action: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build RPi Image + uses: ./.github/actions/build-rpi-image + with: + source_repo: 'jamtools/jamscribe' + version_prefix: 'vjamscribe-' + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 2c7f6e2bad975cde8bf41466234c61961b9054aa Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:30:08 +0000 Subject: [PATCH 33/56] Remove test workflow due to permissions The GitHub App doesn't have workflow permissions, so removing the test workflow file. The composite action is ready to use. --- .github/workflows/test-build-action.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/test-build-action.yml diff --git a/.github/workflows/test-build-action.yml b/.github/workflows/test-build-action.yml deleted file mode 100644 index fe15d7c..0000000 --- a/.github/workflows/test-build-action.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Test RPi Image Build Action - -on: - workflow_dispatch: - push: - paths: - - '.github/actions/build-rpi-image/**' - -jobs: - test-action: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build RPi Image - uses: ./.github/actions/build-rpi-image - with: - source_repo: 'jamtools/jamscribe' - version_prefix: 'vjamscribe-' - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 51d419933f3af10d9764242bfc3a0e3a099cf13c Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Sun, 31 Aug 2025 22:41:58 -0400 Subject: [PATCH 34/56] move GH action steps around --- .github/actions/build-rpi-image/action.yml | 49 +++++++++++----------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 7d28173..9b21b03 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -15,6 +15,30 @@ inputs: runs: using: 'composite' steps: + + - name: Free up disk space + uses: insightsengineering/disk-space-reclaimer@v1 + with: + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: true + docker-images: true + tools-cache: false + + - name: Check disk space + shell: bash + run: df -h + + - name: Update apt + shell: bash + run: sudo apt-get update + + - name: Install Dependencies + shell: bash + run: sudo apt install coreutils p7zip-full qemu-user-static python3-git + - name: Find latest matching release by tag prefix id: find_release shell: bash @@ -42,29 +66,6 @@ runs: out: previous_release.zip token: ${{ inputs.github_token }} - - name: Free up disk space - uses: insightsengineering/disk-space-reclaimer@v1 - with: - android: true - dotnet: true - haskell: true - large-packages: false - swap-storage: true - docker-images: true - tools-cache: false - - - name: Check disk space - shell: bash - run: df -h - - - name: Update apt - shell: bash - run: sudo apt-get update - - - name: Install Dependencies - shell: bash - run: sudo apt install coreutils p7zip-full qemu-user-static python3-git - - name: Checkout CustomPiOS uses: actions/checkout@v2 with: @@ -122,4 +123,4 @@ runs: uses: actions/upload-artifact@v4 with: name: ${{ steps.artifact.outputs.name }} - path: build.img.gz \ No newline at end of file + path: build.img.gz From d61550d18096ef0898f2288e689a680c10b24a89 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Sun, 31 Aug 2025 22:43:10 -0400 Subject: [PATCH 35/56] edit workflow to use new action --- .github/workflows/build_with_custompios.yml | 67 +++------------------ 1 file changed, 9 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index 4f70c01..bc5575d 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -3,64 +3,15 @@ name: Build Image on: push jobs: - build: + test-action: runs-on: ubuntu-latest steps: - - name: Free up disk space - uses: insightsengineering/disk-space-reclaimer@v1 - with: - android: true - dotnet: true - haskell: true - large-packages: false - swap-storage: true - docker-images: true - tools-cache: false + - name: Checkout + uses: actions/checkout@v4 - - name: Check disk space - run: df -h - - name: Update apt - run: sudo apt-get update - - name: Install Dependencies - run: sudo apt install coreutils p7zip-full qemu-user-static python3-git - - name: Checkout CustomPiOS - uses: actions/checkout@v2 - with: - repository: 'guysoft/CustomPiOS' - path: CustomPiOS - - name: Checkout Project Repository - uses: actions/checkout@v2 - with: - repository: ${{ github.repository }} - path: repository_temp - submodules: true - - name: Move FullPageOS folder - run: | - mv repository_temp/FullPageOS repository - # - name: Setup Deno - # uses: denoland/setup-deno@v2 - # with: - # deno-version: v2.x - # - name: Compile IP Configurator - # run: cd repository/apps/ip_configurator && deno task compile - # - name: Move IP Configurator Executable - # run: | - # cp repository/apps/ip_configurator/ip_configurator_executable repository/src/modules/fullpageos/filesystem/home/pi/apps/ - # chmod +x repository/src/modules/fullpageos/filesystem/home/pi/apps/ip_configurator_executable - - name: Download Raspbian Image - run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' - - name: Update CustomPiOS Paths - run: cd repository/src && ../../CustomPiOS/src/update-custompios-paths - - name: Build Image - run: sudo modprobe loop && cd repository/src && sudo BASE_ARCH=armhf bash -x ./build_dist - - name: Copy Output - run: cp ${{ github.workspace }}/repository/src/workspace/*-raspios-*-lite.img build.img - - name: Zip Output - run: gzip build.img - - name: Set artifact name - id: artifact - run: echo "name=$(date '+%Y%m%d_%H%M%S')_${{ github.sha }}_toucheth.img.gz" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.artifact.outputs.name }} - path: build.img.gz + - name: Build RPi Image + uses: ./.github/actions/build-rpi-image + with: + source_repo: 'jamtools/jamscribe' + version_prefix: 'vjamscribe-' + github_token: ${{ secrets.GITHUB_TOKEN }} From 56afc837d2fa97e9f8235f4723380c1ad24332e5 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 02:50:06 +0000 Subject: [PATCH 36/56] fix composite action calling --- .github/workflows/build_with_custompios.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index bc5575d..8d428b5 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -6,11 +6,15 @@ jobs: test-action: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout releases repo into `.github/composite-actions` + uses: actions/checkout@v3 + with: + repository: jamtools/rpi-deploy + path: .github/composite-actions + ref: ${{ github.sha }} - name: Build RPi Image - uses: ./.github/actions/build-rpi-image + uses: ./.github/composite-actions/.github/actions/build-rpi-image with: source_repo: 'jamtools/jamscribe' version_prefix: 'vjamscribe-' From 2304d658f32dc072218925fe9cb1ad13e17da6e0 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 02:52:47 +0000 Subject: [PATCH 37/56] find/replace jamtools user with pi --- .github/actions/build-rpi-image/action.yml | 6 +++--- .../updatecli/new_stuff/updatecli.d/local_file.yml | 4 ++-- .../new_stuff/updatecli.d/scripts/check_esbuild.js | 4 ++-- .../scripts/common/create_and_run_service.sh | 2 +- .../updatecli.d/scripts/run_from_esbuild.sh | 4 ++-- .../updatecli.d/scripts/run_from_github_release.sh | 12 ++++++------ .../updatecli.d/services/check_esbuild.service | 2 +- .../services/check_github_releases.service | 4 ++-- .../updatecli.d/updatecli_github_commit.yml | 4 ++-- .../filesystem/home/pi/updatecli/pi-init.sh | 2 +- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 9b21b03..1611458 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -89,10 +89,10 @@ runs: shell: bash run: | # Create the directory structure where updatecli expects the zip file on the RPi - mkdir -p repository/src/modules/rpi-deploy/filesystem/home/jamtools/code/artifacts/ + mkdir -p repository/src/modules/rpi-deploy/filesystem/home/pi/code/artifacts/ # Move the downloaded zip to where the updatecli scripts expect it - mv previous_release.zip repository/src/modules/rpi-deploy/filesystem/home/jamtools/code/artifacts/dist.zip - echo "Placed release zip at: repository/src/modules/rpi-deploy/filesystem/home/jamtools/code/artifacts/dist.zip" + mv previous_release.zip repository/src/modules/rpi-deploy/filesystem/home/pi/code/artifacts/dist.zip + echo "Placed release zip at: repository/src/modules/rpi-deploy/filesystem/home/pi/code/artifacts/dist.zip" - name: Download Raspbian Image shell: bash diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml index d964ea9..bec3c43 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml @@ -10,9 +10,9 @@ conditions: kind: shell sourceid: fileHash spec: - command: /home/jamtools/code/scripts/compare_with_file.sh ./.last_index_hash + command: /home/pi/code/scripts/compare_with_file.sh ./.last_index_hash targets: fetchBinary: kind: shell spec: - command: /home/jamtools/code/scripts/run_from_esbuild.sh myapp + command: /home/pi/code/scripts/run_from_esbuild.sh myapp diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js index 3e682a6..db49e3b 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js @@ -11,8 +11,8 @@ setTimeout(async () => { }); const run = () => { - spawn('/home/jamtools/code/scripts/run_from_esbuild.sh', ['myapp'], { - // spawn('updatecli', ['apply', '--config', '/home/jamtools/code/local_file.yml'], { + spawn('/home/pi/code/scripts/run_from_esbuild.sh', ['myapp'], { + // spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { env: { PATH: process.env.PATH, }, diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh index ce91ff0..4e33eaa 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh @@ -17,7 +17,7 @@ After=network.target Type=simple ExecStart=$COMMAND Restart=on-failure -WorkingDirectory=/home/jamtools/code/artifacts +WorkingDirectory=/home/pi/code/artifacts [Install] WantedBy=multi-user.target" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh index 9551351..2f0e0a6 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh @@ -2,6 +2,6 @@ set -e mkdir -p artifacts -curl -sL -o /home/jamtools/code/artifacts/index.js http://jam.local:1380/index.js +curl -sL -o /home/pi/code/artifacts/index.js http://jam.local:1380/index.js -/home/jamtools/code/scripts/common/create_and_run_service.sh $1 "node /home/jamtools/code/artifacts/index.js" +/home/pi/code/scripts/common/create_and_run_service.sh $1 "node /home/pi/code/artifacts/index.js" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh index c3f2906..3a62abc 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh @@ -2,15 +2,15 @@ set -e -mkdir -p /home/jamtools/code/artifacts -curl -sL -o /home/jamtools/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" +mkdir -p /home/pi/code/artifacts +curl -sL -o /home/pi/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" -mkdir -p /home/jamtools/code/artifacts/dist -unzip -o /home/jamtools/code/artifacts/dist.zip -d /home/jamtools/code/artifacts/dist +mkdir -p /home/pi/code/artifacts/dist +unzip -o /home/pi/code/artifacts/dist.zip -d /home/pi/code/artifacts/dist -/home/jamtools/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/jamtools/code/artifacts/dist/server/dist/local-server.cjs" +/home/pi/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" -source /home/jamtools/code/secrets.env +source /home/pi/code/secrets.env if [ -n "${WEBHOOK_URL}" ]; then echo "Sending webhook notification" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service index 47749a2..c9e2922 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service @@ -4,7 +4,7 @@ After=network.target [Service] Restart=always -ExecStart=/usr/bin/node /home/jamtools/code/scripts/check_esbuild.js +ExecStart=/usr/bin/node /home/pi/code/scripts/check_esbuild.js [Install] WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service index 50126b7..22149b8 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service @@ -4,8 +4,8 @@ After=network.target [Service] Type=oneshot -ExecStart=/usr/bin/updatecli apply --config /home/jamtools/code/updatecli_github_commit.yml -EnvironmentFile=/home/jamtools/code/secrets.env +ExecStart=/usr/bin/updatecli apply --config /home/pi/code/updatecli_github_commit.yml +EnvironmentFile=/home/pi/code/secrets.env [Install] WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml index c142b28..b1aa0df 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml @@ -16,11 +16,11 @@ conditions: kind: shell sourceid: newRelease spec: - command: /home/jamtools/code/scripts/compare_with_file.sh /home/jamtools/code/.last_release_tag + command: /home/pi/code/scripts/compare_with_file.sh /home/pi/code/.last_release_tag targets: runFromRelease: kind: shell sourceid: newRelease spec: - command: /home/jamtools/code/scripts/run_from_github_release.sh jamtools jamscribe + command: /home/pi/code/scripts/run_from_github_release.sh jamtools jamscribe diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh index bd2cdda..91da0ef 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh @@ -34,7 +34,7 @@ setTimeout(async () => { }); const run = () => { - spawn('updatecli', ['apply', '--config', '/home/jamtools/code/local_file.yml'], { + spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { env: { PATH: process.env.PATH, }, From e14dc6485b751e2df9122290460fb1cd87829a66 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 02:59:48 +0000 Subject: [PATCH 38/56] Parameterize UpdateCLI configuration and ensure GITHUB_TOKEN availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add parameterization step to composite action that replaces hardcoded values in updatecli config - Extract owner/repo from source_repo input parameter - Create secrets.env file with GITHUB_TOKEN for runtime access by systemd service - Update updatecli config to use dynamic owner, repository, version prefix, and script parameters - Copy parameterized config to expected location at /home/pi/code/updatecli_github_commit.yml 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Michael Kochell --- .github/actions/build-rpi-image/action.yml | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 1611458..c49d4ea 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -94,6 +94,37 @@ runs: mv previous_release.zip repository/src/modules/rpi-deploy/filesystem/home/pi/code/artifacts/dist.zip echo "Placed release zip at: repository/src/modules/rpi-deploy/filesystem/home/pi/code/artifacts/dist.zip" + - name: Parameterize UpdateCLI configuration + shell: bash + run: | + # Extract owner and repository from source_repo input + OWNER=$(echo "${{ inputs.source_repo }}" | cut -d'/' -f1) + REPOSITORY=$(echo "${{ inputs.source_repo }}" | cut -d'/' -f2) + + # Create the directory for secrets + mkdir -p repository/src/modules/rpi-deploy/filesystem/home/pi/code/ + + # Create secrets.env file with GITHUB_TOKEN + echo "GITHUB_TOKEN=${{ inputs.github_token }}" > repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env + + # Parameterize the updatecli configuration file + UPDATECLI_CONFIG="repository/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml" + + # Replace hardcoded values with parameterized ones + sed -i "s/owner: jamtools/owner: $OWNER/g" "$UPDATECLI_CONFIG" + sed -i "s/repository: jamscribe/repository: $REPOSITORY/g" "$UPDATECLI_CONFIG" + sed -i "s/pattern: \"vjamscribe-.*\$/pattern: \"${{ inputs.version_prefix }}.*\$/g" "$UPDATECLI_CONFIG" + sed -i "s|run_from_github_release.sh jamtools jamscribe|run_from_github_release.sh $OWNER $REPOSITORY|g" "$UPDATECLI_CONFIG" + + # Copy the updatecli config to where the systemd service expects it + cp "$UPDATECLI_CONFIG" "repository/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml" + + echo "Updated UpdateCLI config with:" + echo " Owner: $OWNER" + echo " Repository: $REPOSITORY" + echo " Version prefix: ${{ inputs.version_prefix }}" + echo " Config copied to: /home/pi/code/updatecli_github_commit.yml" + - name: Download Raspbian Image shell: bash run: cd repository/src/image && wget -q -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' From 23321cb69c254387b0f6d063fd0efd531f8c0eef Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 03:11:06 +0000 Subject: [PATCH 39/56] Fix version stripping in GitHub action to prevent double 'v' prefix The blauqs/actions-download-asset action prepends a 'v' to the version, so we need to strip the first character from our tag name (e.g., 'vjamscribe-2' becomes 'jamscribe-2') to avoid 'vvjamscribe-2' format. Co-authored-by: Michael Kochell --- .github/actions/build-rpi-image/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index c49d4ea..338fc65 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -51,7 +51,7 @@ runs: map(select(.tagName | startswith($prefix))) | sort_by(.publishedAt) | last - | if . == null then "none" else .tagName end + | if . == null then "none" else .tagName[1:] end ') echo "latest_tag=$latest" >> $GITHUB_OUTPUT echo "Found latest release: $latest" From 70f09d8592e4e5d3fef019845a753cb5ac5e107a Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 00:04:32 -0400 Subject: [PATCH 40/56] move some files around --- FullPageOS/src/modules/rpi-deploy/config | 41 ----------------- .../home/pi/code/scripts/check_esbuild.js | 22 +++++++++ .../scripts/common/create_and_run_service.sh | 46 +++++++++++++++++++ .../home/pi/code/scripts/compare_with_file.sh | 27 +++++++++++ .../home/pi/code/scripts/run_from_esbuild.sh | 7 +++ .../code/scripts/run_from_github_release.sh | 18 ++++++++ .../home/pi/code/updatecli_github_commit.yml | 26 +++++++++++ .../etc/systemd/system/check_esbuild.service | 10 ++++ .../system/check_github_releases.service | 11 +++++ .../system/check_github_releases.timer | 9 ++++ .../modules/rpi-deploy/start_chroot_script | 11 ++++- 11 files changed, 186 insertions(+), 42 deletions(-) create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js create mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/common/create_and_run_service.sh create mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/compare_with_file.sh create mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh create mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.timer diff --git a/FullPageOS/src/modules/rpi-deploy/config b/FullPageOS/src/modules/rpi-deploy/config index 7458458..e69de29 100644 --- a/FullPageOS/src/modules/rpi-deploy/config +++ b/FullPageOS/src/modules/rpi-deploy/config @@ -1,41 +0,0 @@ -############################################################################### -#override timezone, otherwise use image timezone -[ -n "$FULLPAGEOS_OVERRIDE_TIMEZONE" ] || FULLPAGEOS_OVERRIDE_TIMEZONE=default - -#override locale, otherwise use image locale -# Run locale -a to get a list of the locale names suitable for use in environment variables. Note that the spellings are different from the ones presented in the dpkg-reconfigure list. -# More information at https://wiki.debian.org/Locale#Standard -[ -n "$FULLPAGEOS_OVERRIDE_LOCALE" ] || FULLPAGEOS_OVERRIDE_LOCALE=default - -#override keyboard model and layout, otherwise use image default -# see `man keyboard` for more information and the file -# `/usr/share/X11/xkb/rules/xorg.lst` for the list of models and layouts -[ -n "$FULLPAGEOS_OVERRIDE_KBD_MODEL" ] || FULLPAGEOS_OVERRIDE_KBD_MODEL=default -[ -n "$FULLPAGEOS_OVERRIDE_KBD_LAYOUT" ] || FULLPAGEOS_OVERRIDE_KBD_LAYOUT=default - -#override password, otherwise use image default -[ -n "$FULLPAGEOS_OVERRIDE_PASSWORD" ] || FULLPAGEOS_OVERRIDE_PASSWORD=default - -[ -n "$FULLPAGEOS_INCLUDE_CHROMIUM" ] || FULLPAGEOS_INCLUDE_CHROMIUM=yes -[ -n "$FULLPAGEOS_INCLUDE_LIGHTTPD" ] || FULLPAGEOS_INCLUDE_LIGHTTPD=yes - -# FullPageDashboard repo & branch -[ -n "$FULLPAGEOS_DASHBOARD_REPO_SHIP" ] || FULLPAGEOS_DASHBOARD_REPO_SHIP=https://github.com/amitdar/FullPageDashboard.git -[ -n "$FULLPAGEOS_DASHBOARD_REPO_BUILD" ] || FULLPAGEOS_DASHBOARD_REPO_BUILD= -[ -n "$FULLPAGEOS_DASHBOARD_REPO_BRANCH" ] || FULLPAGEOS_DASHBOARD_REPO_BRANCH=master -[ -n "$FULLPAGEOS_INCLUDE_DASHBOARD" ] || FULLPAGEOS_INCLUDE_DASHBOARD=yes - -# FullPageDashboard repo & branch -[ -n "$FULLPAGEOS_WELCOME_REPO_SHIP" ] || FULLPAGEOS_WELCOME_REPO_SHIP=https://github.com/tailorvj/FullPageOSWelcome.git -[ -n "$FULLPAGEOS_WELCOME_REPO_BUILD" ] || FULLPAGEOS_WELCOME_REPO_BUILD= -[ -n "$FULLPAGEOS_WELCOME_REPO_BRANCH" ] || FULLPAGEOS_WELCOME_REPO_BRANCH=master -[ -n "$FULLPAGEOS_INCLUDE_WELCOME" ] || FULLPAGEOS_INCLUDE_WELCOME=yes - -# Add GPU acceleration -[ -n "$FULLPAGEOS_INCLUDE_ACCELERATION" ] || FULLPAGEOS_INCLUDE_ACCELERATION=yes - -# Enable custom Splashscreen. Put your picture into FullPageOS/modules/fullpageos/filesystem/home/pi/media/splash.png -[ -n "$FULLPAGEOS_CUSTOM_SPLASHSCREEN" ] || FULLPAGEOS_CUSTOM_SPLASHSCREEN=yes - -# Install and enable x11vnc service -[ -n "$FULLPAGEOS_INCLUDE_X11VNC" ] || FULLPAGEOS_INCLUDE_X11VNC=yes diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js new file mode 100644 index 0000000..db49e3b --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js @@ -0,0 +1,22 @@ +const {EventSource} = require('eventsource'); +const {spawn} = require('node:child_process'); + +const origin = 'http://jam.local:1380'; + +setTimeout(async () => { + run(); + new EventSource(origin + '/esbuild').addEventListener('change', async e => { + run(); + }); +}); + +const run = () => { + spawn('/home/pi/code/scripts/run_from_esbuild.sh', ['myapp'], { + // spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { + env: { + PATH: process.env.PATH, + }, + cwd: process.cwd(), + stdio: 'inherit', + }); +}; diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/common/create_and_run_service.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/common/create_and_run_service.sh new file mode 100755 index 0000000..4e33eaa --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/common/create_and_run_service.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -e + +SECONDS=0 + +SERVICE_NAME="$1" +COMMAND="$2" + +SYSTEMD_PATH="/etc/systemd/system/${SERVICE_NAME}.service" + +NEW_SERVICE_CONTENT="[Unit] +Description=$SERVICE_NAME +After=network.target + +[Service] +Type=simple +ExecStart=$COMMAND +Restart=on-failure +WorkingDirectory=/home/pi/code/artifacts + +[Install] +WantedBy=multi-user.target" + +echo "Before check took $SECONDS seconds" +if [ ! -f "$SYSTEMD_PATH" ] || ! echo "$NEW_SERVICE_CONTENT" | sudo diff - "$SYSTEMD_PATH" >/dev/null 2>&1; then + echo "Service file needs updating..." + echo "$NEW_SERVICE_CONTENT" | sudo tee "$SYSTEMD_PATH" > /dev/null + echo "Before reload took $SECONDS seconds" + sudo systemctl daemon-reload + echo "After reload took $SECONDS seconds" +else + echo "Service file is up to date" +fi + +echo "Before restart took $SECONDS seconds" + +echo "Restarting service..." +sudo systemctl enable "$SERVICE_NAME" + +echo "Before restart took $SECONDS seconds" +sudo systemctl restart "$SERVICE_NAME" + +echo "After restart took $SECONDS seconds" + +echo "Done!" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/compare_with_file.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/compare_with_file.sh new file mode 100755 index 0000000..8c636af --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/compare_with_file.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# set -euo pipefail + +COMPARE_FILE="$1" +SOURCE_VALUE="${2:-}" + +# If SOURCE_VALUE is empty, use last argument passed (from Updatecli) +if [ -z "$SOURCE_VALUE" ]; then + SOURCE_VALUE="${@: -1}" +fi + +if [ ! -f "$COMPARE_FILE" ]; then + echo "Compare file not found, creating with value: $SOURCE_VALUE" + echo "$SOURCE_VALUE" > "$COMPARE_FILE" + exit 0 +fi + +EXISTING_VALUE=$(cat "$COMPARE_FILE") + +if [ "$EXISTING_VALUE" = "$SOURCE_VALUE" ]; then + echo "No change needed. Value matches $COMPARE_FILE." + exit 1 +else + echo "Value changed. Updating $COMPARE_FILE." + echo "$SOURCE_VALUE" > "$COMPARE_FILE" + exit 0 +fi diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh new file mode 100755 index 0000000..2f0e0a6 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -e + +mkdir -p artifacts +curl -sL -o /home/pi/code/artifacts/index.js http://jam.local:1380/index.js + +/home/pi/code/scripts/common/create_and_run_service.sh $1 "node /home/pi/code/artifacts/index.js" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh new file mode 100755 index 0000000..3a62abc --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e + +mkdir -p /home/pi/code/artifacts +curl -sL -o /home/pi/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" + +mkdir -p /home/pi/code/artifacts/dist +unzip -o /home/pi/code/artifacts/dist.zip -d /home/pi/code/artifacts/dist + +/home/pi/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" + +source /home/pi/code/secrets.env + +if [ -n "${WEBHOOK_URL}" ]; then + echo "Sending webhook notification" + curl -X POST -H "Content-Type: application/json" -d '{"text":"new release deployed"}' "${WEBHOOK_URL}" +fi diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml new file mode 100644 index 0000000..cb0a190 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml @@ -0,0 +1,26 @@ +name: Pull latest release on GitHub +sources: + newRelease: + kind: githubrelease + # name: github version + spec: + owner: {{GITHUB_OWNER}} + repository: {{GITHUB_REPOSITORY}} + token: "{{ requiredEnv `GITHUB_TOKEN` }}" + versionfilter: + kind: regex + pattern: "v{{GITHUB_REPOSITORY}}-.*$" + +conditions: + checkIfCommitIsNew: + kind: shell + sourceid: newRelease + spec: + command: /home/pi/code/scripts/compare_with_file.sh /home/pi/code/.last_release_tag + +targets: + runFromRelease: + kind: shell + sourceid: newRelease + spec: + command: /home/pi/code/scripts/run_from_github_release.sh {{GITHUB_OWNER}} {{GITHUB_REPOSITORY}} diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service new file mode 100644 index 0000000..c9e2922 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service @@ -0,0 +1,10 @@ +[Unit] +Description=Check esbuild output file hash and deploy if changed +After=network.target + +[Service] +Restart=always +ExecStart=/usr/bin/node /home/pi/code/scripts/check_esbuild.js + +[Install] +WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service new file mode 100644 index 0000000..b646a5e --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service @@ -0,0 +1,11 @@ +[Unit] +Description=Check GitHub releases +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/updatecli apply --config /home/pi/code/updatecli_github_commit.yml +EnvironmentFile=/home/pi/secrets.env + +[Install] +WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.timer b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.timer new file mode 100644 index 0000000..5b6312b --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Timer for checking GitHub releases + +[Timer] +OnBootSec=1min +OnUnitActiveSec=1hour + +[Install] +WantedBy=timers.target diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index 2342443..9293279 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -12,7 +12,7 @@ source /common.sh unpack /filesystem/home /home # unpack /filesystem/opt /opt # unpack /filesystem/boot /"${BASE_BOOT_MOUNT_PATH}" -# unpack /filesystem/root_init / +unpack /filesystem/root_init / apt-get update @@ -26,6 +26,15 @@ curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/rele npm init -y npm i eventsource +# Make scripts executable +chmod +x /home/pi/code/scripts/*.sh +chmod +x /home/pi/code/scripts/common/*.sh + +# Enable systemd services +systemctl daemon-reload +systemctl enable check_github_releases.timer +systemctl enable check_github_releases.service + echo "server time.nist.gov" >> /etc/ntp.conf echo "server ntp.ubuntu.com" >> /etc/ntp.conf From a7a6f6fbbc80673152184f53c0d62224df65c90b Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:20:34 -0400 Subject: [PATCH 41/56] delete old folder --- .../filesystem/home/pi/updatecli/Dockerfile | 66 ----------- .../home/pi/updatecli/docker-compose.yml | 47 -------- .../new_stuff/updatecli.d/github_release.yml | 22 ---- .../new_stuff/updatecli.d/local_file.yml | 18 --- .../updatecli.d/scripts/check_esbuild.js | 22 ---- .../scripts/common/create_and_run_service.sh | 46 -------- .../updatecli.d/scripts/compare_with_file.sh | 27 ----- .../updatecli.d/scripts/run_from_esbuild.sh | 7 -- .../scripts/run_from_github_release.sh | 18 --- .../services/check_esbuild.service | 10 -- .../services/check_github_releases.service | 11 -- .../services/check_github_releases.timer | 9 -- .../updatecli.d/updatecli_github_commit.yml | 26 ----- .../filesystem/home/pi/updatecli/pi-init.sh | 108 ------------------ .../pi/updatecli/systemd/install_updatecli.sh | 48 -------- .../system/check_github_releases.service | 2 +- 16 files changed, 1 insertion(+), 486 deletions(-) delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/Dockerfile delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/docker-compose.yml delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/github_release.yml delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js delete mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh delete mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh delete mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh delete mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml delete mode 100755 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh delete mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/Dockerfile b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/Dockerfile deleted file mode 100644 index ffd14c1..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -# FROM ubuntu:22.04 -FROM python:3 - -ENV DEBIAN_FRONTEND=noninteractive - -RUN rm -rf /var/lib/apt/lists/* -RUN apt-get update - -RUN apt-get install --allow-downgrades --fix-missing -y systemd systemd-sysv npm - -# FROM python:3 - -# ENV DEBIAN_FRONTEND=noninteractive - -# RUN apt-get update - -RUN mkdir -p /workspace/app -RUN mkdir -p /workspace/runners -RUN mkdir -p /workspace/data - -RUN curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_arm64.deb && \ - dpkg -i /tmp/updatecli_arm64.deb && \ - rm /tmp/updatecli_arm64.deb - -ARG NODE_VERSION=22 - -ENV PATH="$HOME/.fnm:$PATH" - -# RUN apt-get install -y unzip && \ -# curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir /usr/local/fnm && \ -# ln -s /usr/local/fnm/fnm /usr/local/bin/fnm && \ -# eval "$(fnm env)" && \ -# fnm install $NODE_VERSION && \ -# fnm default $NODE_VERSION && fnm use $NODE_VERSION - -# RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash -# ENV NVM_DIR=/root/.nvm -# RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} -# RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} -# RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} -# ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" -# RUN node --version -# RUN npm --version - -# RUN corepack enable pnpm - -RUN mkdir -p /app -WORKDIR /app - -RUN npm init -y -RUN npm i eventsource -COPY new_stuff/updatecli.d/scripts/check_esbuild.js . - -# # WORKDIR /workspace/app - -# # COPY ./workspace/repo_config.json /workspace/repo_config.json - -# # COPY ./dist-pkg/index-linux-x64 ./index - -RUN pip3 install ansible --break-system-packages - -RUN apt-get install --allow-downgrades --fix-missing -y cockpit - -VOLUME [ "/sys/fs/cgroup" ] -STOPSIGNAL SIGRTMIN+3 -CMD ["/sbin/init"] diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/docker-compose.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/docker-compose.yml deleted file mode 100644 index 7d0f6db..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/docker-compose.yml +++ /dev/null @@ -1,47 +0,0 @@ -services: - updatecli: - build: - context: . - # image: updatecli/updatecli:latest - # platform: linux/x86_64 - container_name: updatecli - # restart: unless-stopped - volumes: - # Map the local updatecli configuration into the container (read-only) - - ./new_stuff/updatecli.d:/etc/updatecli - # - ./new_stuff/updatecli.d/local_file.yml:/etc/updatecli/updatecli.yaml:ro - # - ./updatecli.yaml:/etc/updatecli/updatecli.yaml:ro - # Map the local app directory so updatecli can read and update your files - # - ./app:/app:rw - # Map /tmp from the host so temporary files are available - - ./tmp:/tmp:rw - - - /sys/fs/cgroup:/sys/fs/cgroup:rw # Let systemd mount cgroups - - tmpfs:/run # Mount tmpfs at /run - - tmpfs:/run/lock - # environment: - # # Provide your GitHub token if needed - # GITHUB_TOKEN: ${GITHUB_TOKEN} - privileged: true - cgroup: host - # command: /sbin/init - stdin_open: true - tty: true - - # command: "ls /etc/updatecli" - # command: "updatecli apply --config /etc/updatecli/local_file.yml" - # command: "apply --config /etc/updatecli/local_file.yml" - # command: "apply --config /etc/updatecli/updatecli.yaml" - - network_mode: "host" - # command: "manifest show --config /etc/updatecli/updatecli.yaml --debug" - # command: "--help" - # command: "apply --help" - # command: "apply --config /etc/updatecli/updatecli.yaml --debug" - -volumes: - tmpfs: - driver: local - driver_opts: - type: tmpfs - device: tmpfs diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/github_release.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/github_release.yml deleted file mode 100644 index 1360cc1..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/github_release.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Pull latest GitHub release if changed -sources: - githubRelease: - kind: githubRelease - name: github version - spec: - owner: x - repository: y - token: "{{ requiredEnv `GITHUB_TOKEN` }}" -conditions: - alwaysTrue: - kind: shell - spec: - command: echo "true" -targets: - fetchBinary: - kind: shell - spec: - command: | - curl -L -o /opt/updatecli/artifacts/myapp https://github.com/x/y/releases/download/{{ source.githubRelease.version }}/myapp - chmod +x /opt/updatecli/artifacts/myapp - ansible-playbook /opt/updatecli/ansible/deploy.yml -e "artifact_name=myapp" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml deleted file mode 100644 index bec3c43..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/local_file.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Watch esbuild output file hash and deploy if changed -sources: - fileHash: - kind: shell - name: local hash - spec: - command: curl -s http://jam.local:1380/index.js | sha256sum | cut -d' ' -f1 -conditions: - hashChanged: - kind: shell - sourceid: fileHash - spec: - command: /home/pi/code/scripts/compare_with_file.sh ./.last_index_hash -targets: - fetchBinary: - kind: shell - spec: - command: /home/pi/code/scripts/run_from_esbuild.sh myapp diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js deleted file mode 100644 index db49e3b..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/check_esbuild.js +++ /dev/null @@ -1,22 +0,0 @@ -const {EventSource} = require('eventsource'); -const {spawn} = require('node:child_process'); - -const origin = 'http://jam.local:1380'; - -setTimeout(async () => { - run(); - new EventSource(origin + '/esbuild').addEventListener('change', async e => { - run(); - }); -}); - -const run = () => { - spawn('/home/pi/code/scripts/run_from_esbuild.sh', ['myapp'], { - // spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { - env: { - PATH: process.env.PATH, - }, - cwd: process.cwd(), - stdio: 'inherit', - }); -}; diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh deleted file mode 100755 index 4e33eaa..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/common/create_and_run_service.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -e - -SECONDS=0 - -SERVICE_NAME="$1" -COMMAND="$2" - -SYSTEMD_PATH="/etc/systemd/system/${SERVICE_NAME}.service" - -NEW_SERVICE_CONTENT="[Unit] -Description=$SERVICE_NAME -After=network.target - -[Service] -Type=simple -ExecStart=$COMMAND -Restart=on-failure -WorkingDirectory=/home/pi/code/artifacts - -[Install] -WantedBy=multi-user.target" - -echo "Before check took $SECONDS seconds" -if [ ! -f "$SYSTEMD_PATH" ] || ! echo "$NEW_SERVICE_CONTENT" | sudo diff - "$SYSTEMD_PATH" >/dev/null 2>&1; then - echo "Service file needs updating..." - echo "$NEW_SERVICE_CONTENT" | sudo tee "$SYSTEMD_PATH" > /dev/null - echo "Before reload took $SECONDS seconds" - sudo systemctl daemon-reload - echo "After reload took $SECONDS seconds" -else - echo "Service file is up to date" -fi - -echo "Before restart took $SECONDS seconds" - -echo "Restarting service..." -sudo systemctl enable "$SERVICE_NAME" - -echo "Before restart took $SECONDS seconds" -sudo systemctl restart "$SERVICE_NAME" - -echo "After restart took $SECONDS seconds" - -echo "Done!" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh deleted file mode 100755 index 8c636af..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/compare_with_file.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# set -euo pipefail - -COMPARE_FILE="$1" -SOURCE_VALUE="${2:-}" - -# If SOURCE_VALUE is empty, use last argument passed (from Updatecli) -if [ -z "$SOURCE_VALUE" ]; then - SOURCE_VALUE="${@: -1}" -fi - -if [ ! -f "$COMPARE_FILE" ]; then - echo "Compare file not found, creating with value: $SOURCE_VALUE" - echo "$SOURCE_VALUE" > "$COMPARE_FILE" - exit 0 -fi - -EXISTING_VALUE=$(cat "$COMPARE_FILE") - -if [ "$EXISTING_VALUE" = "$SOURCE_VALUE" ]; then - echo "No change needed. Value matches $COMPARE_FILE." - exit 1 -else - echo "Value changed. Updating $COMPARE_FILE." - echo "$SOURCE_VALUE" > "$COMPARE_FILE" - exit 0 -fi diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh deleted file mode 100755 index 2f0e0a6..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_esbuild.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -e - -mkdir -p artifacts -curl -sL -o /home/pi/code/artifacts/index.js http://jam.local:1380/index.js - -/home/pi/code/scripts/common/create_and_run_service.sh $1 "node /home/pi/code/artifacts/index.js" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh deleted file mode 100755 index 3a62abc..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/scripts/run_from_github_release.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -e - -mkdir -p /home/pi/code/artifacts -curl -sL -o /home/pi/code/artifacts/dist.zip "https://github.com/$1/$2/releases/download/$3/dist.zip" - -mkdir -p /home/pi/code/artifacts/dist -unzip -o /home/pi/code/artifacts/dist.zip -d /home/pi/code/artifacts/dist - -/home/pi/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" - -source /home/pi/code/secrets.env - -if [ -n "${WEBHOOK_URL}" ]; then - echo "Sending webhook notification" - curl -X POST -H "Content-Type: application/json" -d '{"text":"new release deployed"}' "${WEBHOOK_URL}" -fi diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service deleted file mode 100644 index c9e2922..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_esbuild.service +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Check esbuild output file hash and deploy if changed -After=network.target - -[Service] -Restart=always -ExecStart=/usr/bin/node /home/pi/code/scripts/check_esbuild.js - -[Install] -WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service deleted file mode 100644 index 22149b8..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Check GitHub releases -After=network.target - -[Service] -Type=oneshot -ExecStart=/usr/bin/updatecli apply --config /home/pi/code/updatecli_github_commit.yml -EnvironmentFile=/home/pi/code/secrets.env - -[Install] -WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer deleted file mode 100644 index 5b6312b..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/services/check_github_releases.timer +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Timer for checking GitHub releases - -[Timer] -OnBootSec=1min -OnUnitActiveSec=1hour - -[Install] -WantedBy=timers.target diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml deleted file mode 100644 index b1aa0df..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Pull latest release on GitHub -sources: - newRelease: - kind: githubrelease - # name: github version - spec: - owner: jamtools - repository: jamscribe - token: "{{ requiredEnv `GITHUB_TOKEN` }}" - versionfilter: - kind: regex - pattern: "vjamscribe-.*$" - -conditions: - checkIfCommitIsNew: - kind: shell - sourceid: newRelease - spec: - command: /home/pi/code/scripts/compare_with_file.sh /home/pi/code/.last_release_tag - -targets: - runFromRelease: - kind: shell - sourceid: newRelease - spec: - command: /home/pi/code/scripts/run_from_github_release.sh jamtools jamscribe diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh deleted file mode 100755 index 91da0ef..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/pi-init.sh +++ /dev/null @@ -1,108 +0,0 @@ - -mkdir code && cd code -sudo apt update -sudo apt install -y cockpit - -# you can then log into cockpit via web browser at https://jamscribe.local:9090 -# you use your actual linux user's creds to log in - -curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_arm64.deb && \ - sudo dpkg -i /tmp/updatecli_arm64.deb && \ - rm /tmp/updatecli_arm64.deb - -sudo apt install -y npm -# this command took a loooooong time to run -# let's see if we can avoid installing this way - -# also it took a while for the pi to initialize at first -# after a while, the ssh command worked - - -mkdir -p scripts - -echo " -const {EventSource} = require('eventsource'); -const {spawn} = require('node:child_process'); - -const origin = 'http://jam.local:1380'; - -setTimeout(async () => { - run(); - new EventSource('http://jam.local:1380/esbuild').addEventListener('change', async e => { - run(); - }); -}); - -const run = () => { - spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { - env: { - PATH: process.env.PATH, - }, - cwd: process.cwd(), - stdio: 'inherit', - }); -}; -" > scripts/check_esbuild.js - -echo ' -#!/bin/bash -# set -e - -COMPARE_FILE="$1" -SOURCE_VALUE="${2:-}" - -# If SOURCE_VALUE is empty, use last argument passed (from Updatecli) -if [ -z "$SOURCE_VALUE" ]; then - SOURCE_VALUE="${@: -1}" -fi - -if [ ! -f "$COMPARE_FILE" ]; then - echo "Compare file not found, creating with value: $SOURCE_VALUE" - echo "$SOURCE_VALUE" > "$COMPARE_FILE" - exit 0 -fi - -EXISTING_VALUE=$(cat "$COMPARE_FILE") - -if [ "$EXISTING_VALUE" = "$SOURCE_VALUE" ]; then - echo "No change needed. Value matches $COMPARE_FILE." - exit 0 -else - echo "Value changed. Updating $COMPARE_FILE." - echo "$SOURCE_VALUE" > "$COMPARE_FILE" - exit 0 -fi -' > scripts/compare_with_file.sh - -chmod +x scripts/compare_with_file.sh - -echo ' -' > scripts/run_from_esbuild.sh - -chmod +x scripts/run_from_esbuild.sh - -echo " -name: Watch esbuild output file hash and deploy if changed -sources: - fileHash: - kind: shell - name: local hash - spec: - command: curl -s http://jam.local:1380/index.js | sha256sum | cut -d' ' -f1 -conditions: - hashChanged: - kind: shell - sourceid: fileHash - spec: - command: ./scripts/compare_with_file.sh ./.last_index_hash -targets: - fetchBinary: - kind: shell - spec: - command: ./scripts/run_from_esbuild.sh -" > local_file.yml - -npm init -y -npm i eventsource - -node scripts/check_esbuild.js diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh deleted file mode 100644 index 6233b13..0000000 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/updatecli/systemd/install_updatecli.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -# Define the desired version of updatecli -UPDATECLI_VERSION="v0.93.0" -ARCH=$(uname -m) - -# Determine the architecture -case $ARCH in - x86_64) - ARCH="amd64" - ;; - aarch64) - ARCH="arm64" - ;; - armv6l) - ARCH="armv6" - ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; -esac - -# Installation directory -INSTALL_DIR="/usr/local/bin" - -# Check if updatecli is already installed and at the desired version -if command -v updatecli &>/dev/null; then - INSTALLED_VERSION=$(updatecli version | awk '{print $3}') - if [ "$INSTALLED_VERSION" == "$UPDATECLI_VERSION" ]; then - echo "updatecli $UPDATECLI_VERSION is already installed." - exit 0 - else - echo "Updating updatecli from version $INSTALLED_VERSION to $UPDATECLI_VERSION." - fi -else - echo "Installing updatecli $UPDATECLI_VERSION." -fi - -# Download and install updatecli -TMP_DIR=$(mktemp -d) -curl -sL -o "$TMP_DIR/updatecli_${ARCH}.tgz" "https://github.com/updatecli/updatecli/releases/download/$UPDATECLI_VERSION/updatecli_${ARCH}.tgz" -tar -xzf "$TMP_DIR/updatecli_${ARCH}.tgz" -C "$TMP_DIR" -mv "$TMP_DIR/updatecli" "$INSTALL_DIR/updatecli" -chmod +x "$INSTALL_DIR/updatecli" -rm -rf "$TMP_DIR" - -echo "updatecli $UPDATECLI_VERSION has been installed successfully." diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service index b646a5e..22149b8 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_github_releases.service @@ -5,7 +5,7 @@ After=network.target [Service] Type=oneshot ExecStart=/usr/bin/updatecli apply --config /home/pi/code/updatecli_github_commit.yml -EnvironmentFile=/home/pi/secrets.env +EnvironmentFile=/home/pi/code/secrets.env [Install] WantedBy=multi-user.target From 5733b98ad105d10ca58e21d91bccbac629ad7fb1 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:40:27 -0400 Subject: [PATCH 42/56] support esbuild for dev --- .github/actions/build-rpi-image/action.yml | 67 +++++++++++++------ .../home/pi/code/esbuild.env.example | 8 +++ .../filesystem/home/pi/code/local_file.yml | 18 +++++ .../home/pi/code/scripts/check_esbuild.js | 13 ++-- .../home/pi/code/scripts/run_from_esbuild.sh | 9 ++- .../code/scripts/run_from_github_release.sh | 2 +- .../etc/systemd/system/check_esbuild.service | 1 + .../modules/rpi-deploy/start_chroot_script | 7 ++ 8 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/esbuild.env.example create mode 100644 FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/local_file.yml diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 338fc65..139368b 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -2,15 +2,22 @@ name: 'Build RPi Image with Release Download' description: 'Downloads latest release from a repo and builds a Raspberry Pi image' inputs: source_repo: - description: 'GitHub repository to download release from (e.g., jamtools/jamscribe)' + description: 'GitHub repository to download release from (e.g., owner/repo)' required: true version_prefix: - description: 'Version tag prefix to match releases (e.g., vjamscribe-)' + description: 'Version tag prefix to match releases (e.g., vapp-)' required: true github_token: description: 'GitHub token for API access' required: true default: ${{ github.token }} + service_name: + description: 'Name of the service to create' + required: false + default: 'myapp' + esbuild_server_url: + description: 'URL of esbuild development server (enables esbuild mode when provided)' + required: false runs: using: 'composite' @@ -100,30 +107,52 @@ runs: # Extract owner and repository from source_repo input OWNER=$(echo "${{ inputs.source_repo }}" | cut -d'/' -f1) REPOSITORY=$(echo "${{ inputs.source_repo }}" | cut -d'/' -f2) - + # Create the directory for secrets mkdir -p repository/src/modules/rpi-deploy/filesystem/home/pi/code/ - - # Create secrets.env file with GITHUB_TOKEN + + # Create secrets.env file with GITHUB_TOKEN and SERVICE_NAME echo "GITHUB_TOKEN=${{ inputs.github_token }}" > repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env - + echo "SERVICE_NAME=${{ inputs.service_name }}" >> repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env + # Parameterize the updatecli configuration file - UPDATECLI_CONFIG="repository/src/modules/rpi-deploy/filesystem/home/pi/updatecli/new_stuff/updatecli.d/updatecli_github_commit.yml" - - # Replace hardcoded values with parameterized ones - sed -i "s/owner: jamtools/owner: $OWNER/g" "$UPDATECLI_CONFIG" - sed -i "s/repository: jamscribe/repository: $REPOSITORY/g" "$UPDATECLI_CONFIG" - sed -i "s/pattern: \"vjamscribe-.*\$/pattern: \"${{ inputs.version_prefix }}.*\$/g" "$UPDATECLI_CONFIG" - sed -i "s|run_from_github_release.sh jamtools jamscribe|run_from_github_release.sh $OWNER $REPOSITORY|g" "$UPDATECLI_CONFIG" - - # Copy the updatecli config to where the systemd service expects it - cp "$UPDATECLI_CONFIG" "repository/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml" - + UPDATECLI_CONFIG="repository/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml" + + # Replace template variables using envsubst + export GITHUB_OWNER="$OWNER" + export GITHUB_REPOSITORY="$REPOSITORY" + export GITHUB_VERSION_PREFIX="${{ inputs.version_prefix }}" + + # Process the template file + envsubst '$GITHUB_OWNER $GITHUB_REPOSITORY' < "$UPDATECLI_CONFIG" > "$UPDATECLI_CONFIG.tmp" + mv "$UPDATECLI_CONFIG.tmp" "$UPDATECLI_CONFIG" + + # Update the version pattern separately since it needs special handling + sed -i "s/v{{GITHUB_REPOSITORY}}-.*\$/v${GITHUB_VERSION_PREFIX}-.*\$/g" "$UPDATECLI_CONFIG" + + # Configure esbuild if server URL is provided + if [ -n "${{ inputs.esbuild_server_url }}" ]; then + echo "ESBUILD_SERVER_URL=${{ inputs.esbuild_server_url }}" >> repository/src/modules/rpi-deploy/filesystem/home/pi/code/esbuild.env + echo "SERVICE_NAME=${{ inputs.service_name }}-dev" >> repository/src/modules/rpi-deploy/filesystem/home/pi/code/esbuild.env + + # Enable esbuild service in start_chroot_script + sed -i 's/# systemctl enable check_esbuild.service/systemctl enable check_esbuild.service/' repository/src/modules/rpi-deploy/start_chroot_script + + echo "Esbuild mode enabled:" + echo " Server URL: ${{ inputs.esbuild_server_url }}" + echo " Service name: ${{ inputs.service_name }}-dev" + fi + echo "Updated UpdateCLI config with:" echo " Owner: $OWNER" - echo " Repository: $REPOSITORY" + echo " Repository: $REPOSITORY" echo " Version prefix: ${{ inputs.version_prefix }}" - echo " Config copied to: /home/pi/code/updatecli_github_commit.yml" + echo " Service name: ${{ inputs.service_name }}" + if [ -n "${{ inputs.esbuild_server_url }}" ]; then + echo " Esbuild mode: enabled" + else + echo " Esbuild mode: disabled (GitHub releases only)" + fi - name: Download Raspbian Image shell: bash diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/esbuild.env.example b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/esbuild.env.example new file mode 100644 index 0000000..b5ca1f5 --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/esbuild.env.example @@ -0,0 +1,8 @@ +# Esbuild configuration for local development mode +# Copy this file to esbuild.env and customize as needed + +# URL of your local esbuild development server +ESBUILD_SERVER_URL=http://localhost:1380 + +# Name of the systemd service to create/update +SERVICE_NAME=myapp-dev \ No newline at end of file diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/local_file.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/local_file.yml new file mode 100644 index 0000000..6bc741e --- /dev/null +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/local_file.yml @@ -0,0 +1,18 @@ +name: Watch esbuild output file hash and deploy if changed +sources: + fileHash: + kind: shell + name: local hash + spec: + command: curl -s ${ESBUILD_SERVER_URL:-http://localhost:1380}/index.js | sha256sum | cut -d' ' -f1 +conditions: + hashChanged: + kind: shell + sourceid: fileHash + spec: + command: /home/pi/code/scripts/compare_with_file.sh /home/pi/code/.last_index_hash +targets: + fetchBinary: + kind: shell + spec: + command: /home/pi/code/scripts/run_from_esbuild.sh ${SERVICE_NAME:-myapp-dev} diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js index db49e3b..c8ca3e2 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/check_esbuild.js @@ -1,7 +1,10 @@ const {EventSource} = require('eventsource'); const {spawn} = require('node:child_process'); -const origin = 'http://jam.local:1380'; +const origin = process.env.ESBUILD_SERVER_URL || 'http://localhost:1380'; +const serviceName = process.env.SERVICE_NAME || 'myapp-dev'; + +console.log(`Starting esbuild watcher for ${origin} with service ${serviceName}`); setTimeout(async () => { run(); @@ -11,12 +14,14 @@ setTimeout(async () => { }); const run = () => { - spawn('/home/pi/code/scripts/run_from_esbuild.sh', ['myapp'], { + spawn('/home/pi/code/scripts/run_from_esbuild.sh', [serviceName], { // spawn('updatecli', ['apply', '--config', '/home/pi/code/local_file.yml'], { env: { - PATH: process.env.PATH, + ...process.env, + ESBUILD_SERVER_URL: origin, + SERVICE_NAME: serviceName, }, - cwd: process.cwd(), + cwd: '/home/pi/code', stdio: 'inherit', }); }; diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh index 2f0e0a6..0a968da 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_esbuild.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash set -e -mkdir -p artifacts -curl -sL -o /home/pi/code/artifacts/index.js http://jam.local:1380/index.js +ESBUILD_SERVER_URL="${ESBUILD_SERVER_URL:-http://localhost:1380}" +SERVICE_NAME="${1:-myapp-dev}" -/home/pi/code/scripts/common/create_and_run_service.sh $1 "node /home/pi/code/artifacts/index.js" +mkdir -p /home/pi/code/artifacts +curl -sL -o /home/pi/code/artifacts/index.js "${ESBUILD_SERVER_URL}/index.js" + +/home/pi/code/scripts/common/create_and_run_service.sh "$SERVICE_NAME" "node /home/pi/code/artifacts/index.js" diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh index 3a62abc..a197504 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh @@ -8,7 +8,7 @@ curl -sL -o /home/pi/code/artifacts/dist.zip "https://github.com/$1/$2/releases/ mkdir -p /home/pi/code/artifacts/dist unzip -o /home/pi/code/artifacts/dist.zip -d /home/pi/code/artifacts/dist -/home/pi/code/scripts/common/create_and_run_service.sh myapp "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" +/home/pi/code/scripts/common/create_and_run_service.sh "${SERVICE_NAME:-myapp}" "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" source /home/pi/code/secrets.env diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service index c9e2922..2beef3e 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/root_init/etc/systemd/system/check_esbuild.service @@ -5,6 +5,7 @@ After=network.target [Service] Restart=always ExecStart=/usr/bin/node /home/pi/code/scripts/check_esbuild.js +EnvironmentFile=-/home/pi/code/esbuild.env [Install] WantedBy=multi-user.target diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index 9293279..60f8eb9 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -23,8 +23,11 @@ curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/rele sudo dpkg -i /tmp/updatecli_arm64.deb && \ rm /tmp/updatecli_arm64.deb +# Install Node.js dependencies in the correct directory +cd /home/pi/code npm init -y npm i eventsource +cd / # Make scripts executable chmod +x /home/pi/code/scripts/*.sh @@ -38,6 +41,10 @@ systemctl enable check_github_releases.service echo "server time.nist.gov" >> /etc/ntp.conf echo "server ntp.ubuntu.com" >> /etc/ntp.conf +# Optional: Enable esbuild service for local development +# Uncomment the following line if you want to use local esbuild server instead of GitHub releases +# systemctl enable check_esbuild.service + #cleanup apt-get clean apt-get autoremove -y From 8674bf2212eb78abc05d9dd355d360345bfebbd1 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:42:25 -0400 Subject: [PATCH 43/56] skip "test" ci job --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83f0e13..cfd4980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: run: npm run check-types test: + if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From df056f315f1d28626152ec977978aaef2598efa4 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 01:43:04 -0400 Subject: [PATCH 44/56] rename ci job --- .github/workflows/build_with_custompios.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index 8d428b5..2e2b59b 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -3,7 +3,7 @@ name: Build Image on: push jobs: - test-action: + build-image: runs-on: ubuntu-latest steps: - name: Checkout releases repo into `.github/composite-actions` From 72c79aa149dd5f69e72076557a67f3debcebddc2 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:28:28 -0400 Subject: [PATCH 45/56] rename ci artifact --- .github/actions/build-rpi-image/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 139368b..1e6288a 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -177,7 +177,7 @@ runs: - name: Set artifact name id: artifact shell: bash - run: echo "name=$(date '+%Y%m%d_%H%M%S')_${{ github.sha }}_${{ inputs.version_prefix }}rpi.img.gz" >> $GITHUB_OUTPUT + run: echo "name=$(date '+%Y%m%d_%H%M%S')_${{ github.sha }}_$(echo '${{ inputs.source_repo }}' | tr '/' '_')_rpi.img.gz" >> $GITHUB_OUTPUT - name: Upload artifact uses: actions/upload-artifact@v4 From 05059e3c50b2e02b745bad2aa3e5a8d92f23507c Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:28:46 -0400 Subject: [PATCH 46/56] include rpi-deploy module when building custompios --- FullPageOS/src/config | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/FullPageOS/src/config b/FullPageOS/src/config index 8b8479f..65af7b9 100755 --- a/FullPageOS/src/config +++ b/FullPageOS/src/config @@ -1,11 +1,11 @@ export DIST_NAME=FullpageOS export DIST_VERSION=1.0.0 -export MODULES="base(raspicam, network, disable-services(gui(fullpageos), usage-statistics))" +export MODULES="base(raspicam, network, disable-services(gui(fullpageos), usage-statistics), rpi-deploy)" # if set will enlarge root parition prior to build by provided size in MB export BASE_IMAGE_ENLARGEROOT=2000 -# if set will resize root partition on image after build to minimum size + +# if set will resize root partition on image after build to minimum size + # provided size in MB export BASE_IMAGE_RESIZEROOT=200 export GUI_STARTUP_SCRIPT=/opt/custompios/scripts/run_onepageos @@ -15,4 +15,3 @@ export RPI_IMAGER_DESCRIPTION="A raspberrypi distro to display a full page brows export RPI_IMAGER_ICON="https://raw.githubusercontent.com/guysoft/FullPageOS/devel/media/rpi-imager-FullPageOS.png" export USAGE_STATISTICS_URL=https://fullpageos-tracking.gnethomelinux.com - From 0c766698d4b58bb4548c4f9a19468483da757ccd Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:27:32 -0400 Subject: [PATCH 47/56] change updatecli arch --- FullPageOS/src/modules/rpi-deploy/start_chroot_script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index 60f8eb9..996f9d7 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -19,7 +19,7 @@ apt-get update sudo apt install -y cockpit sudo apt install -y npm -curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_arm64.deb && \ +curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_armv6.deb && \ sudo dpkg -i /tmp/updatecli_arm64.deb && \ rm /tmp/updatecli_arm64.deb From fabb5a7ab2c5384751659be6be497970c6a16635 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:55:13 -0400 Subject: [PATCH 48/56] unpack as pi user --- FullPageOS/src/modules/rpi-deploy/start_chroot_script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index 996f9d7..9b22687 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -9,7 +9,7 @@ set -e source /common.sh -unpack /filesystem/home /home +unpack /filesystem/home /home pi # unpack /filesystem/opt /opt # unpack /filesystem/boot /"${BASE_BOOT_MOUNT_PATH}" unpack /filesystem/root_init / From a3874a27f720238460772ebdb31553054aa31b8c Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:55:28 -0400 Subject: [PATCH 49/56] install better_sqlite3 and easymidi during image build --- FullPageOS/src/modules/rpi-deploy/start_chroot_script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index 9b22687..a5141f8 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -26,7 +26,7 @@ curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/rele # Install Node.js dependencies in the correct directory cd /home/pi/code npm init -y -npm i eventsource +npm i eventsource better_sqlite3 easymidi cd / # Make scripts executable From 4327f6b3e5a248828f009c4c9d6178103737f20b Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:31:22 -0400 Subject: [PATCH 50/56] fix pakcage name typo --- FullPageOS/src/modules/rpi-deploy/start_chroot_script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index a5141f8..5a659ae 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -26,7 +26,7 @@ curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/rele # Install Node.js dependencies in the correct directory cd /home/pi/code npm init -y -npm i eventsource better_sqlite3 easymidi +npm i eventsource better-sqlite3 easymidi cd / # Make scripts executable From f1658edf16c3e9285371225d1e6a430cb52b9df6 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:00:34 -0400 Subject: [PATCH 51/56] fix substitution issues. add more gh action args for customization --- .github/actions/build-rpi-image/action.yml | 26 ++++++++++++++++--- .github/workflows/build_with_custompios.yml | 4 +++ .../code/scripts/run_from_github_release.sh | 2 +- .../home/pi/code/updatecli_github_commit.yml | 2 +- .../modules/rpi-deploy/start_chroot_script | 3 ++- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 1e6288a..76e631e 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -18,6 +18,14 @@ inputs: esbuild_server_url: description: 'URL of esbuild development server (enables esbuild mode when provided)' required: false + chroot_commands: + description: 'Additional commands to run in chroot environment (multiline string)' + required: false + default: '' + service_command: + description: 'Command to run for the service (e.g., "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs")' + required: false + default: '/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs' runs: using: 'composite' @@ -122,13 +130,25 @@ runs: export GITHUB_OWNER="$OWNER" export GITHUB_REPOSITORY="$REPOSITORY" export GITHUB_VERSION_PREFIX="${{ inputs.version_prefix }}" + export GITHUB_TAG_PREFIX="${GITHUB_VERSION_PREFIX}" # Process the template file - envsubst '$GITHUB_OWNER $GITHUB_REPOSITORY' < "$UPDATECLI_CONFIG" > "$UPDATECLI_CONFIG.tmp" + envsubst '$GITHUB_OWNER $GITHUB_REPOSITORY $GITHUB_TAG_PREFIX' < "$UPDATECLI_CONFIG" > "$UPDATECLI_CONFIG.tmp" mv "$UPDATECLI_CONFIG.tmp" "$UPDATECLI_CONFIG" - # Update the version pattern separately since it needs special handling - sed -i "s/v{{GITHUB_REPOSITORY}}-.*\$/v${GITHUB_VERSION_PREFIX}-.*\$/g" "$UPDATECLI_CONFIG" + # Export additional template variables + export CHROOT_COMMANDS="${{ inputs.chroot_commands }}" + export SERVICE_COMMAND="${{ inputs.service_command }}" + + # Process start_chroot_script template + CHROOT_SCRIPT="repository/src/modules/rpi-deploy/start_chroot_script" + envsubst '$CHROOT_COMMANDS' < "$CHROOT_SCRIPT" > "$CHROOT_SCRIPT.tmp" + mv "$CHROOT_SCRIPT.tmp" "$CHROOT_SCRIPT" + + # Process run_from_github_release.sh template + RELEASE_SCRIPT="repository/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh" + envsubst '$SERVICE_COMMAND' < "$RELEASE_SCRIPT" > "$RELEASE_SCRIPT.tmp" + mv "$RELEASE_SCRIPT.tmp" "$RELEASE_SCRIPT" # Configure esbuild if server URL is provided if [ -n "${{ inputs.esbuild_server_url }}" ]; then diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index 2e2b59b..34f778b 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -19,3 +19,7 @@ jobs: source_repo: 'jamtools/jamscribe' version_prefix: 'vjamscribe-' github_token: ${{ secrets.GITHUB_TOKEN }} + service_name: 'jamscribe' + service_command: '/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs' + chroot_commands: | + npm i better-sqlite3 easymidi diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh index a197504..456d2bc 100755 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/scripts/run_from_github_release.sh @@ -8,7 +8,7 @@ curl -sL -o /home/pi/code/artifacts/dist.zip "https://github.com/$1/$2/releases/ mkdir -p /home/pi/code/artifacts/dist unzip -o /home/pi/code/artifacts/dist.zip -d /home/pi/code/artifacts/dist -/home/pi/code/scripts/common/create_and_run_service.sh "${SERVICE_NAME:-myapp}" "/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs" +/home/pi/code/scripts/common/create_and_run_service.sh "${SERVICE_NAME:-myapp}" "${SERVICE_COMMAND}" source /home/pi/code/secrets.env diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml index cb0a190..0e9193e 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml @@ -9,7 +9,7 @@ sources: token: "{{ requiredEnv `GITHUB_TOKEN` }}" versionfilter: kind: regex - pattern: "v{{GITHUB_REPOSITORY}}-.*$" + pattern: "{{GITHUB_TAG_PREFIX}}.*$" conditions: checkIfCommitIsNew: diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index 5a659ae..f8da2eb 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -26,7 +26,8 @@ curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/rele # Install Node.js dependencies in the correct directory cd /home/pi/code npm init -y -npm i eventsource better-sqlite3 easymidi +npm i eventsource +${CHROOT_COMMANDS} cd / # Make scripts executable From d7498ac507b5b103b0b4b3ac614b8dc8ee8e8ce2 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:10:49 -0400 Subject: [PATCH 52/56] upgrade node to v22 --- FullPageOS/src/modules/rpi-deploy/start_chroot_script | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FullPageOS/src/modules/rpi-deploy/start_chroot_script b/FullPageOS/src/modules/rpi-deploy/start_chroot_script index f8da2eb..8965969 100755 --- a/FullPageOS/src/modules/rpi-deploy/start_chroot_script +++ b/FullPageOS/src/modules/rpi-deploy/start_chroot_script @@ -17,7 +17,9 @@ unpack /filesystem/root_init / apt-get update sudo apt install -y cockpit -sudo apt install -y npm + +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs curl -sL -o /tmp/updatecli_arm64.deb https://github.com/updatecli/updatecli/releases/download/v0.97.0/updatecli_armv6.deb && \ sudo dpkg -i /tmp/updatecli_arm64.deb && \ From 1d7fd70da81229cdb1e362a0a9c2b3811f00b416 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:13:24 -0400 Subject: [PATCH 53/56] fix variable substitutions for real --- .github/actions/build-rpi-image/action.yml | 10 +++++----- .github/workflows/build_with_custompios.yml | 2 +- .../home/pi/code/updatecli_github_commit.yml | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 76e631e..6813dc6 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -7,7 +7,7 @@ inputs: version_prefix: description: 'Version tag prefix to match releases (e.g., vapp-)' required: true - github_token: + github_releases_token: description: 'GitHub token for API access' required: true default: ${{ github.token }} @@ -58,7 +58,7 @@ runs: id: find_release shell: bash env: - GH_TOKEN: ${{ inputs.github_token }} + GH_TOKEN: ${{ inputs.github_releases_token }} run: | PREFIX="${{ inputs.version_prefix }}" latest=$(gh release list --repo "${{ inputs.source_repo }}" --limit 100 --json tagName,publishedAt \ @@ -79,7 +79,7 @@ runs: version: ${{ steps.find_release.outputs.latest_tag }} file: dist.zip out: previous_release.zip - token: ${{ inputs.github_token }} + token: ${{ inputs.github_releases_token }} - name: Checkout CustomPiOS uses: actions/checkout@v2 @@ -119,8 +119,8 @@ runs: # Create the directory for secrets mkdir -p repository/src/modules/rpi-deploy/filesystem/home/pi/code/ - # Create secrets.env file with GITHUB_TOKEN and SERVICE_NAME - echo "GITHUB_TOKEN=${{ inputs.github_token }}" > repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env + # Create secrets.env file with GITHUB_RELEASES_TOKEN and SERVICE_NAME + echo "GITHUB_RELEASES_TOKEN=${{ inputs.github_releases_token }}" > repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env echo "SERVICE_NAME=${{ inputs.service_name }}" >> repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env # Parameterize the updatecli configuration file diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index 34f778b..4517631 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -18,7 +18,7 @@ jobs: with: source_repo: 'jamtools/jamscribe' version_prefix: 'vjamscribe-' - github_token: ${{ secrets.GITHUB_TOKEN }} + github_releases_token: ${{ secrets.GITHUB_RELEASES_TOKEN }} service_name: 'jamscribe' service_command: '/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs' chroot_commands: | diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml index 0e9193e..a943e7c 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml @@ -4,12 +4,12 @@ sources: kind: githubrelease # name: github version spec: - owner: {{GITHUB_OWNER}} - repository: {{GITHUB_REPOSITORY}} - token: "{{ requiredEnv `GITHUB_TOKEN` }}" + owner: ${GITHUB_OWNER} + repository: ${GITHUB_REPOSITORY} + token: "{{ requiredEnv `GITHUB_RELEASES_TOKEN` }}" versionfilter: kind: regex - pattern: "{{GITHUB_TAG_PREFIX}}.*$" + pattern: "${GITHUB_TAG_PREFIX}.*$" conditions: checkIfCommitIsNew: @@ -23,4 +23,4 @@ targets: kind: shell sourceid: newRelease spec: - command: /home/pi/code/scripts/run_from_github_release.sh {{GITHUB_OWNER}} {{GITHUB_REPOSITORY}} + command: /home/pi/code/scripts/run_from_github_release.sh ${GITHUB_OWNER} ${GITHUB_REPOSITORY} From 03576288fcf812d23ff4435a11e15c1aaaab8e01 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:15:47 -0400 Subject: [PATCH 54/56] rename GITHB_RELEASES_TOKEN to GH_RELEASES_TOKEN --- .github/actions/build-rpi-image/action.yml | 10 +++++----- .github/workflows/build_with_custompios.yml | 2 +- .../home/pi/code/updatecli_github_commit.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/build-rpi-image/action.yml b/.github/actions/build-rpi-image/action.yml index 6813dc6..6a578b5 100644 --- a/.github/actions/build-rpi-image/action.yml +++ b/.github/actions/build-rpi-image/action.yml @@ -7,7 +7,7 @@ inputs: version_prefix: description: 'Version tag prefix to match releases (e.g., vapp-)' required: true - github_releases_token: + gh_releases_token: description: 'GitHub token for API access' required: true default: ${{ github.token }} @@ -58,7 +58,7 @@ runs: id: find_release shell: bash env: - GH_TOKEN: ${{ inputs.github_releases_token }} + GH_TOKEN: ${{ inputs.gh_releases_token }} run: | PREFIX="${{ inputs.version_prefix }}" latest=$(gh release list --repo "${{ inputs.source_repo }}" --limit 100 --json tagName,publishedAt \ @@ -79,7 +79,7 @@ runs: version: ${{ steps.find_release.outputs.latest_tag }} file: dist.zip out: previous_release.zip - token: ${{ inputs.github_releases_token }} + token: ${{ inputs.gh_releases_token }} - name: Checkout CustomPiOS uses: actions/checkout@v2 @@ -119,8 +119,8 @@ runs: # Create the directory for secrets mkdir -p repository/src/modules/rpi-deploy/filesystem/home/pi/code/ - # Create secrets.env file with GITHUB_RELEASES_TOKEN and SERVICE_NAME - echo "GITHUB_RELEASES_TOKEN=${{ inputs.github_releases_token }}" > repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env + # Create secrets.env file with GH_RELEASES_TOKEN and SERVICE_NAME + echo "GH_RELEASES_TOKEN=${{ inputs.gh_releases_token }}" > repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env echo "SERVICE_NAME=${{ inputs.service_name }}" >> repository/src/modules/rpi-deploy/filesystem/home/pi/code/secrets.env # Parameterize the updatecli configuration file diff --git a/.github/workflows/build_with_custompios.yml b/.github/workflows/build_with_custompios.yml index 4517631..a823755 100644 --- a/.github/workflows/build_with_custompios.yml +++ b/.github/workflows/build_with_custompios.yml @@ -18,7 +18,7 @@ jobs: with: source_repo: 'jamtools/jamscribe' version_prefix: 'vjamscribe-' - github_releases_token: ${{ secrets.GITHUB_RELEASES_TOKEN }} + gh_releases_token: ${{ secrets.GH_RELEASES_TOKEN }} service_name: 'jamscribe' service_command: '/usr/bin/node /home/pi/code/artifacts/dist/server/dist/local-server.cjs' chroot_commands: | diff --git a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml index a943e7c..15f61ba 100644 --- a/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml +++ b/FullPageOS/src/modules/rpi-deploy/filesystem/home/pi/code/updatecli_github_commit.yml @@ -6,7 +6,7 @@ sources: spec: owner: ${GITHUB_OWNER} repository: ${GITHUB_REPOSITORY} - token: "{{ requiredEnv `GITHUB_RELEASES_TOKEN` }}" + token: "{{ requiredEnv `GH_RELEASES_TOKEN` }}" versionfilter: kind: regex pattern: "${GITHUB_TAG_PREFIX}.*$" From 0cb91115463b5271f6d72c8dbae137e5061ea42a Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:24:32 -0500 Subject: [PATCH 55/56] ad commit to trigger ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfd4980..a64e264 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: run: npm run check-types test: + # skip tests for now if: false runs-on: ubuntu-latest steps: From 4615f5b274df8d62544e467966517c349d3cace9 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:29:44 -0500 Subject: [PATCH 56/56] use "chromium" instead of "chromium-browser" --- FullPageOS/src/modules/fullpageos/start_chroot_script | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FullPageOS/src/modules/fullpageos/start_chroot_script b/FullPageOS/src/modules/fullpageos/start_chroot_script index ce960f8..111bb68 100755 --- a/FullPageOS/src/modules/fullpageos/start_chroot_script +++ b/FullPageOS/src/modules/fullpageos/start_chroot_script @@ -34,11 +34,12 @@ apt-get remove -y --purge $remove_extra apt-get autoremove -y #apt-get tools -apt-get -y --force-yes install git screen checkinstall avahi-daemon libavahi-compat-libdnssd1 xterm xdotool vim expect feh pulseaudio +apt-get -y install git screen checkinstall avahi-daemon libavahi-compat-libdnssd1 xterm xdotool vim expect feh pulseaudio if [ "$FULLPAGEOS_INCLUDE_CHROMIUM" == "yes" ] then - apt-get install -y --force-yes chromium-browser + # Try chromium first (Bookworm/Debian 12+), then chromium-browser (Bullseye) + apt-get install -y chromium || apt-get install -y chromium-browser sed -i 's@%BROWSER_START_SCRIPT%@/opt/custompios/scripts/start_chromium_browser@g' /opt/custompios/scripts/run_onepageos fi @@ -121,7 +122,7 @@ fi #Setup x11vnc if [ "$FULLPAGEOS_INCLUDE_X11VNC" == "yes" ] then - apt-get install -y --force-yes x11vnc + apt-get install -y x11vnc mkdir -p /opt/custompios/vnc chown "${BASE_USER}":"${BASE_USER}" /opt/custompios/vnc