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
9 changes: 9 additions & 0 deletions .github/scripts/dependency-cohort.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ function angularCohort(manifest) {
const fixture = JSON.parse(read('apps/component-demo/test/angular-pptx/package.json'))
test('cold Angular consumer pins coherent framework and tooling groups', () =>
angularCohort(fixture))
test('cold Angular consumer declares the reviewed Vite security runtime', () => {
assert.equal(fixture.overrides['@angular/build'].vite, '8.3.4')
})
test('independently released tooling patches are accepted', () => {
const changed = structuredClone(fixture)
for (const section of ['dependencies', 'devDependencies'])
Expand Down Expand Up @@ -76,6 +79,12 @@ test('missing members, ranges and prerelease versions are rejected', () => {
delete changed.devDependencies['@angular/compiler-cli']
assert.throws(() => angularCohort(changed), /Missing Angular/)
})
test('thumbnail declares and physically installs the reviewed happy-dom security runtime', () => {
const manifest = JSON.parse(read('packages/thumbnail/package.json'))
assert.equal(manifest.devDependencies['happy-dom'], '^21.0.4')
const require = createRequire(new URL('../../packages/thumbnail/package.json', import.meta.url))
assert.equal(require('happy-dom/package.json').version, '21.0.4')
})
test('root and thumbnail declarations agree with the reviewed workspace Vitest cohort', () => {
const root = JSON.parse(read('package.json'))
const thumbnail = JSON.parse(read('packages/thumbnail/package.json'))
Expand Down
62 changes: 62 additions & 0 deletions .github/scripts/prepare-browser-apt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Bound Ubuntu downloads on an ephemeral GitHub-hosted browser runner."""

import argparse
from pathlib import Path
import re


ARCHIVE = "https://archive.ubuntu.com/ubuntu/"
SECURITY = "https://security.ubuntu.com/ubuntu/"
AZURE = "http://azure.archive.ubuntu.com/ubuntu/"
OPTIONS = '''// Browser CI: retain APT's signed metadata and package hash verification.
Acquire::Retries "2";
Acquire::http::Timeout "30";
Acquire::https::Timeout "30";
'''


def prepare(mirrors, config):
# These HTTPS alternatives are already in the official runner mirror list:
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-apt-sources.sh
# A slow but responsive Azure download does not trigger APT's idle timeout.
# Only change that known mirror list, never sources, suites or signing keys.
for target in (mirrors, config):
if target.is_symlink():
raise ValueError("APT preparation refuses symbolic links")
if target.exists() and not target.is_file():
raise ValueError("APT preparation requires regular configuration files")
if not target.parent.is_dir():
raise ValueError("APT preparation requires existing parent directories")
entries = []
for line in mirrors.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
match = re.fullmatch(r"(\S+)\s+priority:\d+", line)
if not match or match[1] not in (AZURE, ARCHIVE, SECURITY):
raise ValueError("Unrecognized runner mirror configuration; no files changed")
entries.append(match[1])
if len(set(entries)) != len(entries) or not {ARCHIVE, SECURITY}.issubset(entries):
raise ValueError("Expected unique official HTTPS archive alternatives; no files changed")
mirrors.write_text(f"{ARCHIVE}\tpriority:1\n{SECURITY}\tpriority:2\n")
config.write_text(OPTIONS)
print("Ubuntu browser downloads: official HTTPS archives, two retries, 30s idle timeout")


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--mirror-list", type=Path, default=Path("/etc/apt/apt-mirrors.txt"))
parser.add_argument(
"--apt-config", type=Path,
default=Path("/etc/apt/apt.conf.d/80-file-viewer-browser"),
)
args = parser.parse_args()
try:
prepare(args.mirror_list, args.apt_config)
except (OSError, ValueError) as error:
parser.exit(1, f"APT preparation failed: {error}\n")


if __name__ == "__main__":
main()
114 changes: 114 additions & 0 deletions .github/scripts/prepare-browser-apt.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'

const script = new URL('./prepare-browser-apt.py', import.meta.url).pathname
const archive = 'https://archive.ubuntu.com/ubuntu/'
const security = 'https://security.ubuntu.com/ubuntu/'
const original = `http://azure.archive.ubuntu.com/ubuntu/\tpriority:1\n${archive}\tpriority:2\n${security}\tpriority:3\n`

function fixture(t, source = original) {
const directory = mkdtempSync(join(tmpdir(), 'file-viewer-apt-'))
t.after(() => rmSync(directory, { recursive: true, force: true }))
const mirrors = join(directory, 'apt-mirrors.txt')
const config = join(directory, '80-file-viewer-browser')
writeFileSync(mirrors, source)
writeFileSync(config, '// original config\n')
return {
directory,
mirrors,
config,
run: () =>
spawnSync('python3', [script, '--mirror-list', mirrors, '--apt-config', config], {
encoding: 'utf8'
})
}
}

test('the actual runner mirror list selects its official HTTPS alternatives and retains trust inputs', (t) => {
const f = fixture(t)
const sources = join(f.directory, 'ubuntu.sources')
const keyring = join(f.directory, 'ubuntu-archive-keyring.gpg')
const source =
'URIs: mirror+file:/etc/apt/apt-mirrors.txt\nSuites: noble noble-security\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n'
writeFileSync(sources, source)
writeFileSync(keyring, 'unchanged signing key fixture')
const result = f.run()
assert.equal(result.status, 0, result.stderr)
assert.equal(readFileSync(f.mirrors, 'utf8'), `${archive}\tpriority:1\n${security}\tpriority:2\n`)
const options = readFileSync(f.config, 'utf8')
assert.match(options, /Acquire::Retries "2"/)
assert.match(options, /Acquire::http::Timeout "30"/)
assert.match(options, /Acquire::https::Timeout "30"/)
assert.doesNotMatch(
options,
/AllowUnauthenticated|AllowInsecure|Verify-Peer|Verify-Host|Check-Valid-Until/
)
assert.equal(readFileSync(sources, 'utf8'), source)
assert.equal(readFileSync(keyring, 'utf8'), 'unchanged signing key fixture')
assert.equal(f.run().status, 0)
assert.equal(readFileSync(f.mirrors, 'utf8'), `${archive}\tpriority:1\n${security}\tpriority:2\n`)
})

for (const [name, source] of [
['unknown mirror', original + 'https://example.invalid/ubuntu/ priority:4\n'],
['missing HTTPS archive', `${security} priority:1\n`],
['insecure alternative', original.replace(archive, 'http://archive.ubuntu.com/ubuntu/')],
['duplicate archive', original + `${archive} priority:4\n`],
['unknown mirror metadata', original.replace('priority:2', 'priority:2 arch:amd64')]
]) {
test(`an ${name} fails before changing either configuration file`, (t) => {
const f = fixture(t, source)
assert.notEqual(f.run().status, 0)
assert.equal(readFileSync(f.mirrors, 'utf8'), source)
assert.equal(readFileSync(f.config, 'utf8'), '// original config\n')
})
}

test('a symlink cannot redirect the privileged config write', (t) => {
const f = fixture(t)
const outside = join(f.directory, 'unrelated-config')
writeFileSync(outside, 'unchanged')
rmSync(f.config)
symlinkSync(outside, f.config)
assert.notEqual(f.run().status, 0)
assert.equal(readFileSync(outside, 'utf8'), 'unchanged')
assert.equal(readFileSync(f.mirrors, 'utf8'), original)
})

test('all browser CI entrypoints prepare APT before a bounded, unchanged browser install', () => {
for (const file of [
'public-ci.yml',
'geo3d-focused-validation.yml',
'geo3d-branch-validation.yml'
]) {
const source = readFileSync(new URL(`../workflows/${file}`, import.meta.url), 'utf8')
const steps = source.split(/\n\s+- name: /)
for (const [index, step] of steps.entries()) {
if (!step.includes('pnpm exec playwright install --with-deps')) continue
assert.match(steps[index - 1], /sudo python3 \.github\/scripts\/prepare-browser-apt\.py/)
assert.match(step, /timeout-minutes: 10/)
assert.match(step, /install --with-deps chromium (?:firefox )?webkit/)
}
}
const workflow = readFileSync(new URL('../workflows/public-ci.yml', import.meta.url), 'utf8')
const fonts = workflow.split('- name: Install the Noto CJK font cohort')[1]?.split('- name:')[0]
assert.ok(fonts)
assert.match(fonts, /timeout-minutes: 10/)
assert.match(fonts, /dpkg-query.*fonts-noto-cjk/)
assert.match(fonts, /apt-get install -y --no-install-recommends fonts-noto-cjk/)
assert.match(fonts, /grep -q 'Noto Serif CJK SC'/)
assert.match(fonts, /font-cohort\.log/)
const browsers = workflow
.split('- name: Verify PDF hand tool and native DOC font table in Chromium and WebKit')[1]
?.split('- name:')[0]
assert.match(browsers, /renderer-pdf verify:hand-tool/)
assert.match(browsers, /@file-viewer\/doc verify:font-table-browser/)
assert.ok(
workflow.indexOf('- name: Install the Noto CJK font cohort') <
workflow.indexOf('- name: Verify PDF hand tool')
)
})
5 changes: 5 additions & 0 deletions .github/workflows/geo3d-branch-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ jobs:
id: focused
run: bash "$RUNNER_TEMP/geo3d-record.sh" geo3d-entry pnpm test:geo3d-entry

- name: Configure bounded Ubuntu archive downloads
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: sudo python3 .github/scripts/prepare-browser-apt.py

- name: Install Chromium and WebKit
id: browsers
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
timeout-minutes: 10
run: bash "$RUNNER_TEMP/geo3d-record.sh" playwright-install pnpm exec playwright install --with-deps chromium webkit

- name: Focused Geo3D browser smoke
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/geo3d-focused-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ jobs:
run: |
mkdir -p output/geo3d-focused
pnpm test:geo3d-entry 2>&1 | tee output/geo3d-focused/entry.log
- name: Configure bounded Ubuntu archive downloads
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: sudo python3 .github/scripts/prepare-browser-apt.py
- name: Install Chromium
id: browser
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
timeout-minutes: 10
run: pnpm exec playwright install --with-deps chromium webkit
- name: Browser datasets and resource ownership
if: >-
Expand Down
33 changes: 29 additions & 4 deletions .github/workflows/public-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ jobs:
if: steps.impact.outputs.runtime != 'false'
run: pnpm --filter @file-viewer/core --filter @file-viewer/renderer-text --filter @file-viewer/capability-mermaid --filter @file-viewer/renderer-drawing --filter @file-viewer/renderer-dicom build

- name: Configure bounded Ubuntu archive downloads
if: steps.impact.outputs.runtime != 'false'
run: sudo python3 .github/scripts/prepare-browser-apt.py

- name: Install Chromium and WebKit
id: browsers
if: steps.impact.outputs.runtime != 'false'
timeout-minutes: 10
run: pnpm exec playwright install --with-deps chromium webkit

- name: Verify DICOM cold consumers, six transfer syntaxes and visible PNGs
Expand Down Expand Up @@ -200,8 +205,13 @@ jobs:
if: steps.impact.outputs.runtime != 'false'
run: pnpm type-check

- name: Configure bounded Ubuntu archive downloads
if: steps.impact.outputs.runtime != 'false'
run: sudo python3 .github/scripts/prepare-browser-apt.py

- name: Install Chromium, Firefox and WebKit
if: steps.impact.outputs.runtime != 'false'
timeout-minutes: 10
run: pnpm exec playwright install --with-deps chromium firefox webkit

- name: Run the issue regression gates
Expand Down Expand Up @@ -279,17 +289,24 @@ jobs:
if: steps.impact.outputs.runtime != 'false'
run: pnpm --filter @file-viewer/vite-plugin verify:lite-consumer

- name: Verify PDF hand tool and native DOC font table in Chromium and WebKit
- name: Install the Noto CJK font cohort
if: steps.impact.outputs.runtime != 'false'
timeout-minutes: 10
run: |
# WenQuanYi's CJK vertical advances can be zero in Chromium. The
# geometry contract needs the same reviewed Noto cohort as DOCX CI.
sudo apt-get update
sudo apt-get install -y --no-install-recommends fonts-noto-cjk
if ! dpkg-query -W -f='${Status}' fonts-noto-cjk 2>/dev/null | grep -Fxq 'install ok installed'; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends fonts-noto-cjk
fi
mkdir -p output/doc-font-table
fc-match -f '%{family}\n' 'Noto Serif CJK SC' | tee output/doc-font-table/font-cohort.log
grep -q 'Noto Serif CJK SC' output/doc-font-table/font-cohort.log
dpkg-query -W fonts-noto-cjk >> output/doc-font-table/font-cohort.log

- name: Verify PDF hand tool and native DOC font table in Chromium and WebKit
if: steps.impact.outputs.runtime != 'false'
run: |
pnpm --filter @file-viewer/renderer-pdf verify:hand-tool
pnpm --filter @file-viewer/doc verify:font-table-browser

Expand Down Expand Up @@ -376,10 +393,18 @@ jobs:
if: steps.impact.outputs.runtime != 'false'
run: pnpm verify:chm-renderer

- name: Verify Vue 3 toolbar in Chromium
- name: Verify Vue 3 and Web toolbar in Chromium and WebKit
if: steps.impact.outputs.runtime != 'false'
run: pnpm verify:vue3-toolbar-browser

- name: Retain zoom toolbar browser evidence
if: always() && steps.impact.outputs.runtime != 'false'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: zoom-toolbar-${{ github.sha }}
path: output/zoom-toolbar-browser/**
retention-days: 7

- name: Verify untrusted renderer markup sanitization
if: steps.impact.outputs.runtime != 'false'
run: pnpm verify:renderer-sanitization
Expand Down
4 changes: 3 additions & 1 deletion apps/component-demo/scripts/verify-angular-pptx-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const report = {
angular: consumerRequire('@angular/core/package.json').version,
tooling: {
build: consumerRequire('@angular/build/package.json').version,
cli: consumerRequire('@angular/cli/package.json').version
cli: consumerRequire('@angular/cli/package.json').version,
vite: createRequire(consumerRequire.resolve('@angular/build/package.json'))('vite/package.json').version
},
baseHref: '/ui/',
expectedSlides,
Expand All @@ -79,6 +80,7 @@ const acceptedFixture = JSON.parse(
assert.equal(report.angular, acceptedFixture.dependencies['@angular/core'])
assert.equal(report.tooling.build, acceptedFixture.devDependencies['@angular/build'])
assert.equal(report.tooling.cli, acceptedFixture.devDependencies['@angular/cli'])
assert.equal(report.tooling.vite, acceptedFixture.overrides['@angular/build'].vite)
let dev,
server,
page,
Expand Down
Loading
Loading