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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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: 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
2 changes: 1 addition & 1 deletion .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 Down
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lint:
cd template && ../node_modules/.bin/eslint --max-warnings=0 --no-inline-config .
ruff check --ignore-noqa template/scripts template/tests
ruff format --check template/scripts template/tests
shellcheck template/scripts/*.sh
cmp mise.toml template/mise.toml
cmp package.json template/package.json
cmp package-lock.json template/package-lock.json
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quick-template",
"version": "0.1.3",
"version": "0.1.4",
"license": "GPL-3.0-or-later",
"private": true,
"type": "module",
Expand Down
17 changes: 11 additions & 6 deletions template/.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 template/.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 template/.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 template/.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 template/.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
4 changes: 2 additions & 2 deletions template/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion template/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quick-template",
"version": "0.1.3",
"version": "0.1.4",
"license": "GPL-3.0-or-later",
"private": true,
"type": "module",
Expand Down
45 changes: 45 additions & 0 deletions template/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 template/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()