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
23 changes: 11 additions & 12 deletions .github/workflows/appstore-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
OUTPUT: dist/CHANGELOG_RELEASE.md

- name: Upload workflow artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: appstore-publish-${{ steps.resolve_channel.outputs.channel }}
path: dist/appstore-publish
Expand Down Expand Up @@ -236,22 +236,21 @@ jobs:
id: purge_urls
run: |
if [[ "${{ steps.resolve_channel.outputs.channel }}" == "dev" ]]; then
echo "urls=[\"https://artifact.websoft9.com/dev/websoft9/plugin/library/library-dev.zip\",\"https://artifact.websoft9.com/dev/websoft9/plugin/media/media-dev.zip\"]" >> "$GITHUB_OUTPUT"
echo "urls=https://artifact.websoft9.com/dev/websoft9/plugin/library/library-dev.zip https://artifact.websoft9.com/dev/websoft9/plugin/media/media-dev.zip" >> "$GITHUB_OUTPUT"
else
echo "urls=[\"https://artifact.websoft9.com/release/websoft9/plugin/library/library-latest.zip\",\"https://artifact.websoft9.com/release/websoft9/plugin/media/media-latest.zip\"]" >> "$GITHUB_OUTPUT"
echo "urls=https://artifact.websoft9.com/release/websoft9/plugin/library/library-latest.zip https://artifact.websoft9.com/release/websoft9/plugin/media/media-latest.zip" >> "$GITHUB_OUTPUT"
fi

- name: Purge legacy Cloudflare Cache
uses: jakejarvis/cloudflare-purge-action@master
env:
CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE_ID }}
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
uses: NathanVaughn/actions-cloudflare-purge@v4.0.0
with:
PURGE_URLS: ${{ steps.purge_urls.outputs.urls }}
cf_zone: ${{ secrets.CLOUDFLARE_ZONE_ID }}
cf_auth: ${{ secrets.CLOUDFLARE_API_TOKEN }}
files: ${{ steps.purge_urls.outputs.urls }}

- name: Create GitHub Release (release channel only)
if: steps.resolve_channel.outputs.channel == 'release'
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
files: |
dist/appstore-publish/legacy/release/library/library.json
Expand All @@ -278,13 +277,13 @@ jobs:
uses: actions/checkout@v7

- name: Setup Pages
uses: actions/configure-pages@v4
uses: actions/configure-pages@v6

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: '.'

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
82 changes: 0 additions & 82 deletions .github/workflows/i18n.yml

This file was deleted.

11 changes: 8 additions & 3 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Validate PR title
- name: Check PR title format
env:
TITLE: ${{ github.event.pull_request.title }}
PATTERN: '^(feat|fix|docs|ci|chore|refactor|perf|test|build|revert)(\([^)]+\))?!?:[[:space:]]+.+'
run: |
if ! [[ "$TITLE" =~ $PATTERN ]]; then
echo "::error::PR title must look like 'type: summary', e.g. 'fix: handle empty config'"
exit 1
echo "::warning::PR title should look like 'type: summary', e.g. 'fix: handle empty config'"
fi

- name: Checkout repository
Expand All @@ -52,6 +51,12 @@ jobs:
- name: Run build tests
run: python -m pytest tests/build -q

- name: Validate i18n keys for changed apps
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: make i18n-check ARGS="--base $BASE_SHA --head $HEAD_SHA --github-annotations --warn-only"

- name: Validate changed catalog files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Read in this order:
- Translatable env keys are `W9_*_SET` and `W9_LOGIN*`.
- When a task adds or changes such keys, ensure `i18n/translation.json` gets the key.
- New keys may stay `["", ""]`; translations are filled by humans, not AI.
- Do not edit downstream repos; the `i18n.yml` workflow syncs them by PR.
- Do not edit downstream repos. i18n checks cover only this repo's `translation.json`; downstream sync workflows were removed.

## Delivery Model

Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help opencode opencode-clear install libs cli remote connector github-token install-git-hooks dns-bind dns-delete websoft9-upgrade create-aliyun-ecs build-ai-commands test test-cli test-build test-skills
.PHONY: help opencode opencode-clear install libs cli remote connector github-token install-git-hooks dns-bind dns-delete websoft9-upgrade create-aliyun-ecs build-ai-commands i18n-check i18n-update test test-cli test-build test-skills

ifeq ($(OS),Windows_NT)
PYTHON ?= py
Expand Down Expand Up @@ -29,6 +29,8 @@ help:
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'test-build' 'run build pipeline smoke tests'
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'test-skills' 'run skills asset and workflow tests'
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'build-ai-commands' 'generate opencode/copilot/codex command adapters from manifest'
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'i18n-check' 'check translation.json for missing keys (ARGS: --strict, --app <app>)'
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'i18n-update' 'add missing translation keys as ["", ""]'
@printf '\n \033[1;35m▸ Config & Setup\033[0m\n'
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'help' 'show this help'
@printf ' \033[2mmake \033[0m\033[1;32m%-18s\033[0m %s\n' 'install' 'create .venv and install the libs CLI'
Expand Down Expand Up @@ -239,6 +241,12 @@ connector:
chmod 600 "$$file"; \
echo "wrote $$file"'

i18n-check:
$(PYTHON) i18n/manage.py --check $(ARGS)

i18n-update:
$(PYTHON) i18n/manage.py --update $(ARGS)

test-cli:
$(VENV_BIN)/python -m pytest cli/tests -q

Expand Down
5 changes: 5 additions & 0 deletions apps/grafana/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## 2026-10-23

- Added a startup log regression check for unsupported Grafana plugins.
- Version scans now identify patch releases already covered by the current floating minor tag.

## 2026-10-22

- Kept `W9_VERSION=13.2`; the upstream stable `13.2` tag currently resolves to Grafana `13.2.4`.
Expand Down
3 changes: 3 additions & 0 deletions apps/grafana/tests/cases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ optional:
type: web-access
path: /api/health
expect_status: 200
- id: grafana-plugin-validation
type: script
script: check-plugin-validation.sh
22 changes: 22 additions & 0 deletions apps/grafana/tests/check-plugin-validation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail

container="${W9_ID:-grafana}"
stale_plugin="/var/lib/grafana/plugins/grafana-simple-json-datasource"

if docker exec "$container" test -d "$stale_plugin"; then
echo "Unsupported stale plugin remains in the Grafana plugins volume: $stale_plugin" >&2
echo "Remove that directory once before validating an existing instance." >&2
exit 1
fi

started_at="$(docker inspect -f '{{.State.StartedAt}}' "$container")"
logs="$(docker logs --since "$started_at" "$container" 2>&1 || true)"

if grep -Eiq 'Plugin validation failed|angular plugins are not supported|Refusing to initialize plugin' <<<"$logs"; then
echo "Grafana reported an unsupported plugin during this container startup." >&2
grep -Ei 'Plugin validation failed|angular plugins are not supported|Refusing to initialize plugin' <<<"$logs" >&2
exit 1
fi

echo "No unsupported plugin validation errors during the current startup."
2 changes: 1 addition & 1 deletion apps/metabase/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
W9_REPO=metabase/metabase
W9_DIST=community
W9_VERSION=v0.64.x
W9_VERSION=v0.64.1.x

# Optional password seed: enable only when the package actually controls a DB or built-in login.
# See docs/w9-env-spec.md for when W9_POWER_PASSWORD is appropriate.
Expand Down
5 changes: 5 additions & 0 deletions apps/metabase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## 2026-10-23

- Updated Metabase to the official `v0.64.1.x` Docker image tag from the invalid `v0.64.x` placeholder.
- Updated version metadata so upstream scans can parse and compare the current Metabase image correctly.

## 2026-10-08

- Updated Metabase from `v0.57.5.3` to `v0.64.x`.
Expand Down
3 changes: 2 additions & 1 deletion apps/metabase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

1. In the [Websoft9](https://www.websoft9.com) console, open **My Apps** and select **Metabase**.
2. In the **Access** tab, get the login URL and credentials.

3. Open the login URL in a browser and sign in to confirm the app works.

<!-- W9_GUIDE_START -->
Expand Down Expand Up @@ -33,7 +34,7 @@ Apps run as containers; rebuild after any configuration change.

### Version Support

Supported versions: v0.64.x, latest.
Supported versions: v0.64.1.x, latest.

The `latest` tag is not guaranteed to remain valid; pin a specific version for production.

Expand Down
2 changes: 1 addition & 1 deletion apps/metabase/variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{
"dist": "community",
"version": [
"v0.64.x",
"v0.64.1.x",
"latest"
]
}
Expand Down
Binary file modified cli/libs/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/app_deploy.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/http.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/main.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/remote.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/repo.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/sources.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/versioning.cpython-312.pyc
Binary file not shown.
Binary file modified cli/libs/__pycache__/versions.cpython-312.pyc
Binary file not shown.
28 changes: 19 additions & 9 deletions cli/libs/app_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
SS_PORT_RE = re.compile(r":(\d+)\s+\S+\s*$")
FREE_PORT_LOW = 9000
FREE_PORT_HIGH = 9999
PULL_POLICIES = {"always", "missing", "never"}


def _run(command: list[str], *, progress: ProgressWriter | None = None, verbose: bool = False) -> subprocess.CompletedProcess:
Expand Down Expand Up @@ -87,6 +88,10 @@ def _remote_compose_script(target: str, action: str) -> str:
return f"docker compose -f {target}/docker-compose.yml --env-file {target}/.env {action}"


def _compose_up_action(pull_policy: str) -> str:
return "up -d --pull never" if pull_policy == "never" else "up -d"


def _compose_services(target: Path) -> dict:
compose_path = target / "docker-compose.yml"
if not compose_path.exists():
Expand Down Expand Up @@ -388,12 +393,15 @@ def deploy(
version: str | None = None,
down: bool = False,
auto_port: bool = True,
pull_policy: str = "always",
progress: ProgressWriter | None = None,
verbose: bool = False,
) -> dict:
source = app_dir(app_name)
if not source:
raise FileNotFoundError(app_name)
if pull_policy not in PULL_POLICIES:
raise ValueError(f"invalid pull policy: {pull_policy}; expected one of {sorted(PULL_POLICIES)}")

mode = _target_mode(target, ssh_host)
action = "down -v" if down else "up -d"
Expand Down Expand Up @@ -425,9 +433,9 @@ def deploy(
raise RuntimeError(config.stderr.strip() or config.stdout.strip() or "compose config failed")

if not down:
_announce(progress, step + 1, total, "pulling images")
if pull_services:
pull = _run(_local_compose_args(target_path, env_file) + ["pull", *pull_services], progress=progress, verbose=verbose)
_announce(progress, step + 1, total, "pulling images" if pull_policy != "never" else "using existing images")
if pull_services and pull_policy != "never":
pull = _run(_local_compose_args(target_path, env_file) + ["pull", "--policy", pull_policy, *pull_services], progress=progress, verbose=verbose)
if pull.returncode != 0:
raise RuntimeError(pull.stderr.strip() or pull.stdout.strip() or "compose pull failed")
if needs_build:
Expand All @@ -439,11 +447,11 @@ def deploy(
progress=progress,
)
_announce(progress, step + 3, total, "starting application")
result = _run(_local_compose_args(target_path, env_file) + ["up", "-d"], progress=progress, verbose=verbose)
result = _run(_local_compose_args(target_path, env_file) + _compose_up_action(pull_policy).split(), progress=progress, verbose=verbose)
ps_step = step + 4
else:
_announce(progress, step + 2, total, "starting application")
result = _run(_local_compose_args(target_path, env_file) + ["up", "-d"], progress=progress, verbose=verbose)
result = _run(_local_compose_args(target_path, env_file) + _compose_up_action(pull_policy).split(), progress=progress, verbose=verbose)
ps_step = step + 3
else:
_announce(progress, step + 1, total, "stopping application")
Expand All @@ -461,6 +469,7 @@ def deploy(
"deploy_root": str(repo_path("apps")),
"app_target": str(target_path),
"action": action,
"pull_policy": pull_policy,
"port_overrides": {key: str(value) for key, value in overrides.items()},
"base_url": base_url,
"ps": ps.stdout.strip(),
Expand Down Expand Up @@ -505,9 +514,9 @@ def deploy(
raise RuntimeError(config.stderr.strip() or config.stdout.strip() or "remote compose config failed")

if not down:
_announce(progress, step + 1, total, "pulling images")
if pull_services:
pull = _run_remote(host, user, secret_path, _remote_compose_script(app_target, f"pull {' '.join(pull_services)}"), progress=progress, verbose=verbose)
_announce(progress, step + 1, total, "pulling images" if pull_policy != "never" else "using existing images")
if pull_services and pull_policy != "never":
pull = _run_remote(host, user, secret_path, _remote_compose_script(app_target, f"pull --policy {pull_policy} {' '.join(pull_services)}"), progress=progress, verbose=verbose)
if pull.returncode != 0:
raise RuntimeError(pull.stderr.strip() or pull.stdout.strip() or "remote compose pull failed")
if needs_build:
Expand All @@ -522,7 +531,7 @@ def deploy(
progress=progress,
)
_announce(progress, step + 3, total, "starting application")
result = _run_remote(host, user, secret_path, _remote_compose_script(app_target, "up -d"), progress=progress, verbose=verbose)
result = _run_remote(host, user, secret_path, _remote_compose_script(app_target, _compose_up_action(pull_policy)), progress=progress, verbose=verbose)
ps_step = step + 4
else:
_announce(progress, step + 2, total, "starting application")
Expand All @@ -547,6 +556,7 @@ def deploy(
"app_target": app_target,
"version": version,
"action": action,
"pull_policy": pull_policy,
"port_overrides": {key: str(value) for key, value in overrides.items()},
"base_url": base_url,
"ps": ps.stdout.strip(),
Expand Down
Loading
Loading