Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
changelog:
exclude:
labels:
- ignore-for-release
- skip-changelog
categories:
- title: Breaking changes
labels:
- breaking-change
- breaking
- title: Features
labels:
- enhancement
- feature
- title: Bug fixes
labels:
- bug
- bugfix
- title: Maintenance and dependencies
labels:
- maintenance
- dependencies
- chore
- title: Other changes
labels:
- "*"
146 changes: 129 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,151 @@
name: Build release tarball
name: Build and test multiACE packages

on:
push:
branches:
- '**'
tags:
- 'v*'
pull_request:
workflow_dispatch:
inputs:
source_ref:
description: Branch, tag, or commit SHA to build as a test artifact (no release is published)
required: true
default: main
type: string

permissions:
contents: write
contents: read

jobs:
release:
validate:
runs-on: ubuntu-latest
outputs:
source_sha: ${{ steps.source.outputs.sha }}
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref || github.sha }}

- name: Record exact source commit
id: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Validate managed package sources
run: |
python -m py_compile \
multiace/klipper/extras/ace.py \
multiace/klipper/extras/ace_gen1_tunnel.py \
multiace/web/backend/i18n_path.py \
multiace/web/backend/main.py \
multiace/tools/post_process_virtual_toolheads.py \
multiace/managed/build_package.py \
multiace/managed/tests/test_package.py \
multiace/managed/tests/test_i18n_path.py
node --check multiace/web/frontend/app.js
bash -n \
multiace/install_multiace.sh \
multiace/uninstall_multiace.sh \
multiace/tools/multiace_update.sh \
multiace/config/extended/multiace/ace_mode_switch.sh \
multiace/web/deploy/S98multiace-web
bash multiace/managed/tests/test_managed_guards.sh
bash multiace/managed/tests/test_updater_asset_selection.sh
python -m unittest discover -s multiace/managed/tests -v

python multiace/managed/build_package.py \
--output "$RUNNER_TEMP/multiace-managed-check.tar.gz"
(cd "$RUNNER_TEMP" && \
sha256sum --check multiace-managed-check.tar.gz.sha256)

package:
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
needs: validate
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout validated source
uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.source_sha }}

- name: Compose tarball
- name: Build packages and metadata
shell: bash
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}"
OUT="dist/release"
mkdir -p "$OUT"
# The whole multiace/ tree at its repo layout (same as the
# release recipe): the updater runs install_multiace.sh from
# inside, and the installer needs deploy/, config/, tools/,
# i18n/ and web/ next to it.
tar -czf "$OUT/multiace-${TAG}.tar.gz" multiace/
(cd "$OUT" && sha256sum "multiace-${TAG}.tar.gz" \
> "multiace-${TAG}.tar.gz.sha256")
VERSION="$(tr -d '\r\n' < multiace/VERSION)"
SOURCE_SHA="$(git rev-parse HEAD)"
test -n "$VERSION"

if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
BUILD_KIND="manual-test-artifact"
PACKAGE_TAG="v$VERSION-test-$GITHUB_RUN_ID"
else
TAG="$GITHUB_REF_NAME"
test "$TAG" = "v$VERSION"
BUILD_KIND="stable-tag"
PACKAGE_TAG="$TAG"
fi

# Keep the existing standalone archive layout required by its installer.
tar -czf "$OUT/multiace-$PACKAGE_TAG.tar.gz" multiace/
(cd "$OUT" && sha256sum "multiace-$PACKAGE_TAG.tar.gz" \
> "multiace-$PACKAGE_TAG.tar.gz.sha256")
python multiace/managed/build_package.py \
--output "$OUT/multiace-managed-$VERSION.tar.gz"

{
printf 'Build kind: %s\n' "$BUILD_KIND"
printf 'Version: %s\n' "$VERSION"
printf 'Source commit: %s\n' "$SOURCE_SHA"
printf 'Workflow run: https://github.com/%s/actions/runs/%s\n' \
"$GITHUB_REPOSITORY" "$GITHUB_RUN_ID"
} > "$OUT/build-info.txt"

if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ]; then
{
printf '## Release packages\n\n'
printf -- '- Standalone installer: multiace-%s.tar.gz and its SHA-256 checksum.\n' "$TAG"
printf -- '- Platform-managed payload: multiace-managed-%s.tar.gz and its SHA-256 checksum.\n\n' "$VERSION"
printf 'Built from source commit %s (https://github.com/%s/commit/%s).\n' \
"$SOURCE_SHA" "$GITHUB_REPOSITORY" "$SOURCE_SHA"
} > "$OUT/release-prefix.md"
fi
ls -la "$OUT/"

- name: Publish
- name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
name: multiace-packages-${{ github.run_id }}
path: dist/release/*
if-no-files-found: error
retention-days: 14

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [validate, package]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download validated packages
uses: actions/download-artifact@v4
with:
name: multiace-packages-${{ github.run_id }}
path: dist/release

- name: Publish stable release
uses: softprops/action-gh-release@v2
with:
files: |
dist/release/multiace-*.tar.gz
dist/release/multiace-*.tar.gz.sha256
tag_name: ${{ github.ref_name }}
target_commitish: ${{ needs.validate.outputs.source_sha }}
name: ${{ github.ref_name }}
body_path: dist/release/release-prefix.md
generate_release_notes: true
fail_on_unmatched_files: true
files: |
dist/release/*.tar.gz
dist/release/*.tar.gz.sha256
7 changes: 7 additions & 0 deletions multiace/config/extended/multiace/ace_mode_switch.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#!/bin/bash
set -e
if [ "${MULTIACE_MANAGED:-0}" = "1" ] || \
[ "${MULTIACE_MANAGED:-}" = "true" ] || \
[ -e "${MULTIACE_MANAGED_MARKER:-${MULTIACE_CONFIG_DIR:-/home/lava/printer_data/config}/extended/multiace/.multiace-managed}" ]; then
echo "multiACE mode switching is managed by the platform; refusing to copy Klipper files" >&2
exit 2
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
HOME_DIR="/home/lava"
EXTRAS_DIR="${HOME_DIR}/klipper/klippy/extras"
Expand Down
1 change: 1 addition & 0 deletions multiace/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
"update_failed": "Update fehlgeschlagen",
"update_intro": "Holt die neueste multiACE-Release und installiert sie.",
"update_latest": "Verfügbar",
"update_managed": "Updates werden von der Plattform verwaltet. Verwende die multiACE-Integration deines Druckers, um eine geprüfte Version zu installieren.",
"update_not_checked": "noch nicht geprüft",
"update_title": "multiACE Updates"
},
Expand Down
1 change: 1 addition & 0 deletions multiace/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
"update_failed": "Update failed",
"update_intro": "Pulls the latest multiACE release and installs it.",
"update_latest": "Available",
"update_managed": "Updates are managed by the platform. Use your printer's multiACE integration to install a tested release.",
"update_not_checked": "not checked yet",
"update_title": "multiACE updates"
},
Expand Down
1 change: 1 addition & 0 deletions multiace/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"update_intro": "拉取最新的 multiACE 版本并安装。",
"update_current": "已安装",
"update_latest": "可用版本",
"update_managed": "更新由平台管理。请使用打印机的 multiACE 集成安装经过验证的版本。",
"update_not_checked": "尚未检查",
"update_check_btn": "检查更新",
"update_checking": "检查中...",
Expand Down
8 changes: 8 additions & 0 deletions multiace/install_multiace.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/bin/bash
set -e

if [ "${MULTIACE_MANAGED:-0}" = "1" ] || \
[ "${MULTIACE_MANAGED:-}" = "true" ] || \
[ -e "${MULTIACE_MANAGED_MARKER:-${MULTIACE_CONFIG_DIR:-/home/lava/printer_data/config}/extended/multiace/.multiace-managed}" ]; then
echo "multiACE is managed by the platform; use its integration instead of install_multiace.sh" >&2
exit 2
fi

INSTALL_WEB=0
KEEP_CONFIG=0
for arg in "$@"; do
Expand Down
49 changes: 43 additions & 6 deletions multiace/klipper/extras/ace.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
MULTIACE_BUILD_TAG = "eed86c9b"
MULTIACE_BUNDLE_SHA1 = "40a00eb"


def _env_flag(name):
return os.environ.get(name, '').strip().lower() in ('1', 'true', 'yes', 'on')


_MULTIACE_CONFIG_DIR = os.environ.get(
'MULTIACE_CONFIG_DIR', '/home/lava/printer_data/config')
MULTIACE_MANAGED_MARKER = os.environ.get(
'MULTIACE_MANAGED_MARKER', '').strip() or os.path.join(
_MULTIACE_CONFIG_DIR, 'extended', 'multiace', '.multiace-managed')
MULTIACE_MANAGED = (
_env_flag('MULTIACE_MANAGED')
or os.path.exists(MULTIACE_MANAGED_MARKER))

def _load_i18n_catalog(i18n_dir, lang):
"""Read <i18n_dir>/<lang>.json overlaid on en.json. Returns a dict
(possibly empty if the i18n dir is missing) - caller falls back to
Expand Down Expand Up @@ -697,8 +711,9 @@ def __init__(self, config):

self.paths = _resolve_multiace_paths(config)
self.host = self.paths['host_type']
# Derived, never a config option: the web backend edits the same
# file (MULTIACE_CFG_PATH), so the two must not be able to diverge.
self._managed_by_host = MULTIACE_MANAGED
# Derived from the shared config-directory contract so Klipper and
# the web UI always write through to the same ace.cfg.
self.ACE_CFG_PATH = self.paths['ace_cfg']

self.gate_status = [GATE_UNKNOWN, GATE_UNKNOWN, GATE_UNKNOWN, GATE_UNKNOWN]
Expand Down Expand Up @@ -1433,7 +1448,11 @@ def _parse_idx_list(key):
'inbox_max_mb', 256, minval=1, maxval=4096)

self._i18n_primary = config.get('i18n_dir', self.paths['i18n_primary'])
self._i18n_fallback = os.path.join(self._web_dir, 'i18n')
self._i18n_fallbacks = []
app_dir = os.environ.get('MULTIACE_APP_DIR', '').strip()
if app_dir:
self._i18n_fallbacks.append(os.path.join(app_dir, 'i18n'))
self._i18n_fallbacks.append(os.path.join(self._web_dir, 'i18n'))
self._reload_i18n_catalog()

self._head_source = {0: None, 1: None, 2: None, 3: None}
Expand Down Expand Up @@ -2347,8 +2366,9 @@ def _t(self, key, **params):
def _reload_i18n_catalog(self):
"""(Re)load self._i18n for the current self._language. Used at startup
and live by MULTIACE_SET_LANGUAGE."""
i18n_dir = self._i18n_primary if os.path.isdir(self._i18n_primary) \
else self._i18n_fallback
candidates = [self._i18n_primary] + self._i18n_fallbacks
i18n_dir = next((path for path in candidates if os.path.isdir(path)),
self._i18n_primary)
try:
self._i18n = _load_i18n_catalog(i18n_dir, self._language)
except Exception as e:
Expand Down Expand Up @@ -20732,7 +20752,13 @@ def cmd_ACE_RUN_MODE_SWITCH(self, gcmd):
# multi<->head stay on the SAME ace files -> pure runtime flip, no file
# swap / reboot. Only transitions involving 'normal' (stock files) run
# the file switch script below.
if mode in ('multi', 'head') and current in ('multi', 'head'):
if self._managed_by_host and mode == 'normal':
raise gcmd.error(
'[multiACE] Normal mode is controlled by the host platform. '
'Disable the managed multiACE integration and reboot.')

if mode in ('multi', 'head') and (
current in ('multi', 'head') or self._managed_by_host):
self.gcode.run_script_from_command(
"SAVE_VARIABLE VARIABLE=ace__mode VALUE=\"'%s'\"" % mode)
self._ace_mode = mode
Expand Down Expand Up @@ -20766,6 +20792,14 @@ def cmd_ACE_RUN_MODE_SWITCH(self, gcmd):
pass
return

if self._managed_by_host:
# The host has already selected and activated the ACE modules.
# Managed mode changes runtime state only; it never invokes the
# standalone helper that copies over stock Klipper files.
raise gcmd.error(
'[multiACE] This mode change is not supported by the managed '
'runtime; the host platform controls file activation.')

save_vars = self.printer.lookup_object('save_variables')
vars_path = save_vars.filename
script_dir = os.path.dirname(os.path.abspath(vars_path))
Expand Down Expand Up @@ -20818,6 +20852,9 @@ def cmd_ACE_RUN_MODE_SWITCH(self, gcmd):
_UPDATE_SCRIPT = '/home/lava/multiace_update.sh' if os.path.isfile('/home/lava/multiace_update.sh') else os.path.expanduser('~/multiace_update.sh')

def _run_update_script(self, gcmd, sub_args, timeout):
if self._managed_by_host:
raise gcmd.error(
'[multiACE] Updates are managed by the platform.')
if not os.path.isfile(self._UPDATE_SCRIPT):
raise gcmd.error(
'[multiACE] Updater script not found at %s - re-run '
Expand Down
Loading
Loading