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
17 changes: 11 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3
with:
version: 2026.10.1
- name: Install native test tools
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gjs gnome-shell libglib2.0-bin librsvg2-common gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon
run: bash scripts/native_tools.sh
- run: npm ci --ignore-scripts
- run: ./node_modules/.bin/playwright install --with-deps chromium firefox
- name: Install browser engines
run: |
for attempt in 1 2 3; do
./node_modules/.bin/playwright install --with-deps chromium firefox && exit 0
echo "::warning::Playwright install attempt $attempt failed; retrying"
sleep 15
done
exit 1
- run: just ci
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: extension-bundle
path: '*.shell-extension.zip'
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3
with:
version: 2026.10.1
cache: false
Expand Down Expand Up @@ -50,14 +50,14 @@ jobs:
contents: write # Create the release and attach its tested ZIP.
actions: read # Inspect workflow runs and download their tested artifacts.
steps:
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3
with:
version: 2026.10.1
cache: false
mise_toml: |
[tools]
gh = "2.102.0"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: extension-bundle
run-id: ${{ needs.verify.outputs.run }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
- uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: javascript-typescript,python
queries: security-extended
- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
- uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
scanners:
name: scanners
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
Expand All @@ -37,7 +37,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3
with:
version: 2026.10.1
- run: npm ci --ignore-scripts
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3
with:
version: 2026.10.1
- name: Require Sonar configuration
Expand All @@ -31,9 +31,7 @@ jobs:
run: test -n "$SONAR_TOKEN" || { echo '::error::SONAR_TOKEN is required; scan cannot be skipped'; exit 1; }
- run: npm ci --ignore-scripts
- name: Install native test tools
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gjs gnome-shell libglib2.0-bin librsvg2-common gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon
run: bash scripts/native_tools.sh
- run: just coverage
- name: Analyze the checked pull request or main branch
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
- uses: jdx/mise-action@82a612c5ab8bcda7cc5db6b7e81a8e7584c3dc6e # v5.2.3
with:
version: 2026.10.1
- run: just template-check template-status
2 changes: 1 addition & 1 deletion quick-template.lock.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "quick-template.schema.json",
"repository": "Ghost-Assembly/quick-template",
"revision": "cfe18978694f233ef2874a063a8fee236f691943"
"revision": "846ae471eb8c3599d408aad70d8c14c57a91fdc8"
}
45 changes: 45 additions & 0 deletions scripts/native_tools.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Install native test tools on a hosted runner without hanging on a stalled mirror.
# Each download runs under a deadline and is retried; after a failure apt switches
# from the runner's Azure mirror to archive.ubuntu.com. Packages that did arrive
# stay in apt's cache, so a retry resumes. Only the final offline install runs
# dpkg, and nothing interrupts it.
set -euo pipefail

readonly attempts=3
readonly deadline=150 # Seconds per apt command; a healthy mirror needs under ten.
readonly apt=(apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30)
readonly install=(install -y --no-install-recommends gjs gnome-shell libglib2.0-bin
librsvg2-common gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon)

# coreutils timeout runs under sudo so it signals apt itself, not only sudo.
fetch() {
sudo timeout "$deadline" "${apt[@]}" update &&
sudo timeout "$deadline" "${apt[@]}" "${install[@]}" --download-only
}

# Hosted runners list mirrors in apt-mirrors.txt, which apt leaves only on errors.
switch_mirror() {
local apt_dir="$1" stale
mapfile -t stale < <(grep -lsF azure.archive.ubuntu.com "$apt_dir"/apt-mirrors.txt \
"$apt_dir"/sources.list "$apt_dir"/sources.list.d/* || true)
if ((${#stale[@]})); then
sudo sed -i 's/azure[.]archive[.]ubuntu[.]com/archive.ubuntu.com/g' "${stale[@]}"
fi
}

main() {
local apt_dir="${1:-/etc/apt}" attempt
for ((attempt = 1; ; attempt++)); do
fetch && break
if ((attempt == attempts)); then
echo "::error::Native test tools did not download in $attempts attempts"
exit 1
fi
echo "::warning::apt attempt $attempt failed or stalled; retrying via archive.ubuntu.com"
switch_mirror "$apt_dir"
done
sudo "${apt[@]}" "${install[@]}"
}

main "$@"
76 changes: 76 additions & 0 deletions tests/test_tooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io
import json
import os
import shutil
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -576,5 +577,80 @@ def test_cleanup_preserves_source_and_external_symlink_targets(self) -> None:
self.assertEqual(list(root.glob("*.shell-extension.zip")), [])


class NativeToolsTests(unittest.TestCase):
def install(self, codes: list[int]) -> tuple[subprocess.CompletedProcess, list[str], Path]:
"""Run the real installer with a sudo stand-in that replays apt exit codes."""
work = Path(self.enterContext(tempfile.TemporaryDirectory()))
(work / "bin").mkdir()
(work / "codes").write_text("".join(f"{code}\n" for code in codes))
sudo = work / "bin/sudo"
sudo.write_text(
"#!/usr/bin/env bash\n"
'printf "%s\\n" "$*" >> "$WORK/log"\n'
'if [ "$1" = sed ]; then exec "$@"; fi\n'
'code="$(head -n1 "$WORK/codes")"; sed -i 1d "$WORK/codes"; exit "${code:-0}"\n'
)
sudo.chmod(0o755)
(work / "log").touch()
shutil.copyfile(ROOT / "scripts/native_tools.sh", work / "native_tools.sh")
(work / "apt/sources.list.d").mkdir(parents=True)
(work / "apt/apt-mirrors.txt").write_text(
"http://azure.archive.ubuntu.com/ubuntu/\tpriority:1\n"
"https://archive.ubuntu.com/ubuntu/\tpriority:2\n"
)
(work / "apt/sources.list").write_text(
"deb http://azure.archive.ubuntu.com/ubuntu noble main\n"
)
(work / "apt/sources.list.d/ubuntu.sources").write_text(
"URIs: mirror+file:/etc/apt/apt-mirrors.txt\n"
)
(work / "apt/sources.list.d/other.sources").write_text("URIs: https://example.invalid/\n")
result = subprocess.run(
["/usr/bin/env", "bash", "native_tools.sh", "apt"],
cwd=work,
env={**os.environ, "PATH": f"{work / 'bin'}:{os.environ['PATH']}", "WORK": str(work)},
capture_output=True,
text=True,
check=False,
timeout=30,
)
return result, (work / "log").read_text().splitlines(), work / "apt"

def test_healthy_mirror_downloads_once_and_installs_offline(self) -> None:
result, log, apt = self.install([])
self.assertEqual(result.returncode, 0)
self.assertEqual([line.split()[0] for line in log], ["timeout", "timeout", "apt-get"])
self.assertIn("--download-only", log[1])
self.assertNotIn("--download-only", log[2])
self.assertIn("azure.archive", (apt / "apt-mirrors.txt").read_text())

def test_stalled_mirror_switches_only_matching_sources_and_retries(self) -> None:
result, log, apt = self.install([124])
self.assertEqual(result.returncode, 0)
self.assertIn("::warning::apt attempt 1", result.stdout)
self.assertNotIn("azure", (apt / "apt-mirrors.txt").read_text())
self.assertEqual(
(apt / "sources.list").read_text(), "deb http://archive.ubuntu.com/ubuntu noble main\n"
)
for name, contents in [
("ubuntu.sources", "URIs: mirror+file:/etc/apt/apt-mirrors.txt\n"),
("other.sources", "URIs: https://example.invalid/\n"),
]:
self.assertEqual((apt / "sources.list.d" / name).read_text(), contents)
self.assertEqual(sum(line.startswith("sed ") for line in log), 1)
self.assertTrue(log[-1].startswith("apt-get ") and log[-1].endswith("dbus-daemon"))

def test_exhausted_downloads_never_reach_dpkg(self) -> None:
result, log, _ = self.install([0, 1] * 3)
self.assertEqual(result.returncode, 1)
self.assertIn("did not download in 3 attempts", result.stdout)
self.assertTrue(all(line.startswith(("timeout ", "sed ")) for line in log))

def test_failed_install_is_reported(self) -> None:
result, log, _ = self.install([0, 0, 100])
self.assertEqual(result.returncode, 100)
self.assertEqual(len(log), 3)


if __name__ == "__main__":
unittest.main()
Loading