Skip to content

Ecosystem smoke

Ecosystem smoke #436

name: Ecosystem smoke
on:
schedule:
- cron: "17 */4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ecosystem-smoke
cancel-in-progress: true
env:
VASTLINT_VERSION: "0.4.22"
VALID_FIXTURE: crates/vastlint-core/tests/fixtures/valid_4.2.xml
INVALID_FIXTURE: crates/vastlint-core/tests/fixtures/err_no_ad.xml
jobs:
smoke-cli:
name: Smoke published CLI
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v (pinned SHA)
with:
toolchain: stable
- name: Install published vastlint CLI
run: |
INSTALL_ROOT="$RUNNER_TEMP/vastlint-cli"
cargo install --locked vastlint-cli --version "$VASTLINT_VERSION" --root "$INSTALL_ROOT"
echo "$INSTALL_ROOT/bin" >> "$GITHUB_PATH"
- name: Validate fixtures with published CLI
shell: bash
run: |
set -euo pipefail
valid_json="$RUNNER_TEMP/valid-cli.json"
invalid_json="$RUNNER_TEMP/invalid-cli.json"
vastlint check "$VALID_FIXTURE" --format json > "$valid_json"
set +e
vastlint check "$INVALID_FIXTURE" --format json > "$invalid_json"
exit_code=$?
set -e
if [ "$exit_code" -eq 0 ]; then
echo "Expected invalid fixture to fail the CLI smoke test"
exit 1
fi
python3 -c 'import json, sys; valid = json.load(open(sys.argv[1], encoding="utf-8")); invalid = json.load(open(sys.argv[2], encoding="utf-8")); assert valid["summary"]["errors"] == 0, valid; assert invalid["summary"]["errors"] > 0, invalid; print("CLI smoke passed")' "$valid_json" "$invalid_json"
smoke-rust-core:
name: Smoke published Rust crate
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v (pinned SHA)
with:
toolchain: stable
- name: Build disposable Rust app with vastlint-core
shell: bash
run: |
set -euo pipefail
APP_DIR="$RUNNER_TEMP/vastlint-core-smoke"
mkdir -p "$APP_DIR/src"
printf '%s\n' \
'[package]' \
'name = "vastlint-core-smoke"' \
'version = "0.1.0"' \
'edition = "2021"' \
'' \
'[dependencies]' \
"vastlint-core = { version = \"=${VASTLINT_VERSION}\" }" \
> "$APP_DIR/Cargo.toml"
printf '%s\n' \
'use std::{env, fs};' \
'use vastlint_core::validate;' \
'' \
'fn main() {' \
' let mut args = env::args().skip(1);' \
' let valid_path = args.next().expect("valid fixture path");' \
' let invalid_path = args.next().expect("invalid fixture path");' \
'' \
' let valid = validate(&fs::read_to_string(valid_path).expect("read valid fixture"));' \
' assert_eq!(valid.summary.errors, 0, "expected valid fixture to have zero errors");' \
'' \
' let invalid = validate(&fs::read_to_string(invalid_path).expect("read invalid fixture"));' \
' assert!(invalid.summary.errors > 0, "expected invalid fixture to have errors");' \
'' \
' println!("Rust crate smoke passed ({} errors on invalid fixture)", invalid.summary.errors);' \
'}' \
> "$APP_DIR/src/main.rs"
cargo run --quiet --manifest-path "$APP_DIR/Cargo.toml" -- \
"$GITHUB_WORKSPACE/$VALID_FIXTURE" \
"$GITHUB_WORKSPACE/$INVALID_FIXTURE"
smoke-npm:
name: Smoke published npm packages
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false
- name: Install published vastlint npm package in a clean app
shell: bash
run: |
set -euo pipefail
APP_DIR="$RUNNER_TEMP/vastlint-npm-smoke"
mkdir -p "$APP_DIR"
printf '%s\n' \
'{' \
' "name": "vastlint-npm-smoke",' \
' "private": true,' \
' "type": "module"' \
'}' \
> "$APP_DIR/package.json"
pushd "$APP_DIR" >/dev/null
PACKED_TARBALL="$(npm pack "vastlint@${VASTLINT_VERSION}" --silent)"
mkdir -p node_modules
tar -xzf "$PACKED_TARBALL" -C node_modules
mv node_modules/package node_modules/vastlint
rm "$PACKED_TARBALL"
printf '%s\n' \
"import fs from 'node:fs';" \
"import { validate } from 'vastlint';" \
'' \
'const [validPath, invalidPath] = process.argv.slice(2);' \
'' \
"const valid = validate(fs.readFileSync(validPath, 'utf8'));" \
'if (!valid.summary.valid || valid.summary.errors !== 0) {' \
' throw new Error("expected valid fixture to pass: " + JSON.stringify(valid.summary));' \
'}' \
'' \
"const invalid = validate(fs.readFileSync(invalidPath, 'utf8'));" \
'if (invalid.summary.valid || invalid.summary.errors === 0) {' \
' throw new Error("expected invalid fixture to fail: " + JSON.stringify(invalid.summary));' \
'}' \
'' \
'console.log("npm smoke passed (" + invalid.summary.errors + " errors on invalid fixture)");' \
> smoke.mjs
node smoke.mjs \
"$GITHUB_WORKSPACE/$VALID_FIXTURE" \
"$GITHUB_WORKSPACE/$INVALID_FIXTURE"
popd >/dev/null
- name: Install published vastlint-client npm package in a clean app
shell: bash
run: |
set -euo pipefail
APP_DIR="$RUNNER_TEMP/vastlint-client-smoke"
mkdir -p "$APP_DIR"
printf '%s\n' \
'{' \
' "name": "vastlint-client-smoke",' \
' "private": true,' \
' "type": "module"' \
'}' \
> "$APP_DIR/package.json"
pushd "$APP_DIR" >/dev/null
PACKED_TARBALL="$(npm pack "vastlint-client@${VASTLINT_VERSION}" --silent)"
npm install --no-package-lock --ignore-scripts "./$PACKED_TARBALL"
rm "$PACKED_TARBALL"
cat > smoke.mjs <<'EOF'
import fs from 'node:fs';
import {
createVastPlaybackController,
createVastSession,
selectResolvedAdMediaFile,
selectTrackingTargets,
} from 'vastlint-client';
const [validPath, invalidPath] = process.argv.slice(2);
const playbackFixture = `<?xml version="1.0" encoding="UTF-8"?>
<VAST version="4.2">
<Ad id="playback-1">
<InLine>
<AdSystem>Playback Demo</AdSystem>
<AdTitle>Playback Fixture</AdTitle>
<Error><![CDATA[https://track.example.com/error?code=%%ERRORCODE%%]]></Error>
<Impression><![CDATA[https://track.example.com/impression]]></Impression>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:20</Duration>
<TrackingEvents>
<Tracking event="creativeView"><![CDATA[https://track.example.com/creative-view]]></Tracking>
<Tracking event="start"><![CDATA[https://track.example.com/start]]></Tracking>
<Tracking event="firstQuartile"><![CDATA[https://track.example.com/first-quartile]]></Tracking>
</TrackingEvents>
<VideoClicks>
<ClickThrough><![CDATA[https://click.example.com/landing]]></ClickThrough>
<ClickTracking><![CDATA[https://track.example.com/click]]></ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4" width="1280" height="720"><![CDATA[https://cdn.example.com/video.mp4]]></MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>`;
function createPixelResponse() {
return {
ok: true,
status: 204,
statusText: 'No Content',
async text() {
return '';
},
};
}
const validSession = createVastSession({
source: { kind: 'xml', xml: fs.readFileSync(validPath, 'utf8') },
});
const validResult = await validSession.validate();
if (!validResult.summary.valid || validResult.summary.errors !== 0) {
throw new Error('expected valid fixture to pass: ' + JSON.stringify(validResult.summary));
}
const invalidSession = createVastSession({
source: { kind: 'xml', xml: fs.readFileSync(invalidPath, 'utf8') },
});
const invalidResult = await invalidSession.validate();
if (invalidResult.summary.valid || invalidResult.summary.errors === 0) {
throw new Error('expected invalid fixture to fail: ' + JSON.stringify(invalidResult.summary));
}
const trackingCalls = [];
const playbackSession = createVastSession({
source: { kind: 'xml', xml: playbackFixture },
fetch: async (url) => {
trackingCalls.push(String(url));
return createPixelResponse();
},
});
await playbackSession.resolve();
const playbackSnapshot = playbackSession.getSnapshot();
if (!playbackSnapshot.resolvedAd?.resolved) {
throw new Error('expected playback fixture to resolve to an inline ad');
}
const mediaSelection = selectResolvedAdMediaFile(playbackSnapshot.resolvedAd, {
supportedMimeTypes: ['video/mp4'],
});
if (mediaSelection.selected?.url !== 'https://cdn.example.com/video.mp4') {
throw new Error('expected playback fixture media to be selected');
}
const startTargets = selectTrackingTargets(playbackSession.getSnapshot().tracking.plan, 'start');
if (startTargets.length !== 1) {
throw new Error('expected one start tracking target, got ' + startTargets.length);
}
const playback = createVastPlaybackController({
session: playbackSession,
autoResolve: false,
mediaSelection: {
supportedMimeTypes: ['video/mp4'],
},
});
await playback.initialize();
await playback.start();
if (playback.getSnapshot().status !== 'playing') {
throw new Error('expected playback controller to enter playing state');
}
for (const url of [
'https://track.example.com/impression',
'https://track.example.com/creative-view',
'https://track.example.com/start',
]) {
if (!trackingCalls.includes(url)) {
throw new Error('expected playback tracking call for ' + url);
}
}
console.log('vastlint-client smoke passed');
EOF
node smoke.mjs \
"$GITHUB_WORKSPACE/$VALID_FIXTURE" \
"$GITHUB_WORKSPACE/$INVALID_FIXTURE"
popd >/dev/null
- name: Install published vastlint-react npm package in a clean app
shell: bash
run: |
set -euo pipefail
APP_DIR="$RUNNER_TEMP/vastlint-react-smoke"
mkdir -p "$APP_DIR"
printf '%s\n' \
'{' \
' "name": "vastlint-react-smoke",' \
' "private": true,' \
' "type": "module"' \
'}' \
> "$APP_DIR/package.json"
pushd "$APP_DIR" >/dev/null
PACKED_TARBALL="$(npm pack "vastlint-react@${VASTLINT_VERSION}" --silent)"
npm install --no-package-lock --ignore-scripts \
"./$PACKED_TARBALL" \
react@19.2.0 \
react-dom@19.2.0 \
jsdom@26.1.0
rm "$PACKED_TARBALL"
cat > smoke.mjs <<'EOF'
import React, { act } from 'react';
import { JSDOM } from 'jsdom';
import { createRoot } from 'react-dom/client';
import { createVastSession } from 'vastlint-client';
import { useVastPlayback } from 'vastlint-react';
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
function defineGlobal(name, value) {
Object.defineProperty(globalThis, name, {
configurable: true,
writable: true,
value,
});
}
function installDomGlobals(window) {
defineGlobal('window', window);
defineGlobal('document', window.document);
defineGlobal('navigator', window.navigator);
defineGlobal('HTMLElement', window.HTMLElement);
defineGlobal('Node', window.Node);
defineGlobal('Event', window.Event);
defineGlobal('CustomEvent', window.CustomEvent);
defineGlobal('requestAnimationFrame', (callback) => setTimeout(() => callback(Date.now()), 0));
defineGlobal('cancelAnimationFrame', (id) => clearTimeout(id));
}
function createPixelResponse() {
return {
ok: true,
status: 204,
statusText: 'No Content',
async text() {
return '';
},
};
}
const playbackFixture = `<?xml version="1.0" encoding="UTF-8"?>
<VAST version="4.2">
<Ad id="playback-1">
<InLine>
<AdSystem>Playback Demo</AdSystem>
<AdTitle>Playback Fixture</AdTitle>
<Impression><![CDATA[https://track.example.com/impression]]></Impression>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:20</Duration>
<TrackingEvents>
<Tracking event="creativeView"><![CDATA[https://track.example.com/creative-view]]></Tracking>
<Tracking event="start"><![CDATA[https://track.example.com/start]]></Tracking>
</TrackingEvents>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4" width="1280" height="720"><![CDATA[https://cdn.example.com/video.mp4]]></MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>`;
const trackingCalls = [];
const dom = new JSDOM('<!doctype html><html><body></body></html>');
installDomGlobals(dom.window);
async function mountHook(useHook) {
let latestResult = null;
const container = dom.window.document.createElement('div');
dom.window.document.body.append(container);
const root = createRoot(container);
function Probe() {
latestResult = useHook();
return null;
}
await act(async () => {
root.render(React.createElement(Probe));
});
return {
getLatest() {
if (!latestResult) {
throw new Error('expected hook result to be available');
}
return latestResult;
},
async unmount() {
await act(async () => {
root.unmount();
});
},
};
}
const session = createVastSession({
source: { kind: 'xml', xml: playbackFixture },
fetch: async (url) => {
trackingCalls.push(String(url));
return createPixelResponse();
},
});
const mounted = await mountHook(() => useVastPlayback({
session,
autoInitialize: false,
mediaSelection: {
supportedMimeTypes: ['video/mp4'],
},
}));
if (mounted.getLatest().snapshot.status !== 'idle') {
throw new Error('expected hook playback to start idle');
}
await act(async () => {
await mounted.getLatest().initialize();
});
if (mounted.getLatest().snapshot.status !== 'ready') {
throw new Error('expected hook playback to become ready');
}
if (mounted.getLatest().snapshot.resolvedAd?.adTitle !== 'Playback Fixture') {
throw new Error('expected hook playback to expose resolved ad metadata');
}
await act(async () => {
await mounted.getLatest().start();
});
if (mounted.getLatest().snapshot.status !== 'playing') {
throw new Error('expected hook playback to enter playing state');
}
if (mounted.getLatest().snapshot.mediaSelection.selected?.url !== 'https://cdn.example.com/video.mp4') {
throw new Error('expected hook playback to expose selected media metadata');
}
for (const url of [
'https://track.example.com/impression',
'https://track.example.com/creative-view',
'https://track.example.com/start',
]) {
if (!trackingCalls.includes(url)) {
throw new Error('expected hook playback tracking call for ' + url);
}
}
await mounted.unmount();
dom.window.close();
console.log('vastlint-react smoke passed');
EOF
node smoke.mjs
popd >/dev/null
smoke-chrome-extension:
name: Smoke Chrome extension build
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
package-manager-cache: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- name: Build WASM payload for extensions
run: |
wasm-pack build crates/vastlint-wasm --target bundler --out-dir ../../npm/pkg
wasm-pack build crates/vastlint-wasm --target nodejs --out-dir ../../npm/pkg-node
- name: Assemble npm package
run: node npm/scripts/assemble.js
- name: Install Chrome extension dependencies
working-directory: chrome
run: npm ci
- name: Build Chrome extension
working-directory: chrome
run: npm run build
- name: Verify Chrome extension dist output
working-directory: chrome
run: |
REQUIRED=(
dist/manifest.json
dist/content.js
dist/service-worker.js
dist/popup.js
dist/popup.html
dist/vastlint_wasm_bg.wasm
)
MISSING=0
for f in "${REQUIRED[@]}"; do
if [ ! -f "$f" ]; then
echo "MISSING: $f"
MISSING=$((MISSING + 1))
else
echo "OK: $f"
fi
done
[ "$MISSING" -eq 0 ] || { echo "$MISSING required file(s) missing from dist/"; exit 1; }
smoke-vscode-vscodium-extension:
name: Smoke VS Code/VSCodium extension package
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
package-manager-cache: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- name: Build WASM payload for extension npm dependency
run: |
wasm-pack build crates/vastlint-wasm --target bundler --out-dir ../../npm/pkg
wasm-pack build crates/vastlint-wasm --target nodejs --out-dir ../../npm/pkg-node
- name: Assemble npm package
run: node npm/scripts/assemble.js
- name: Install VS Code extension dependencies
working-directory: vscode
run: npm ci
- name: Bundle extension
working-directory: vscode
run: npm run bundle
- name: Package VSIX (usable by VS Code and VSCodium/Open VSX)
working-directory: vscode
run: npx --no-install vsce package --no-update-package-json --out vastlint-smoke.vsix
- name: Verify VSIX contents
working-directory: vscode
run: |
test -s vastlint-smoke.vsix
unzip -l vastlint-smoke.vsix | grep -q 'extension/package.json'
unzip -l vastlint-smoke.vsix | grep -q 'extension/out/extension.js'
smoke-editor-marketplace-install:
name: Smoke editor marketplace installs
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
package-manager-cache: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- name: Build WASM payload for extension npm dependency
run: |
wasm-pack build crates/vastlint-wasm --target bundler --out-dir ../../npm/pkg
wasm-pack build crates/vastlint-wasm --target nodejs --out-dir ../../npm/pkg-node
- name: Assemble npm package
run: node npm/scripts/assemble.js
- name: Build and package VSIX
working-directory: vscode
run: |
npm ci
npm run bundle
npx --no-install vsce package --no-update-package-json --out vastlint-smoke.vsix
- name: Download portable VS Code and VSCodium
shell: bash
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/editors"
curl -fL "https://update.code.visualstudio.com/latest/linux-x64/stable" -o "$RUNNER_TEMP/editors/vscode.tar.gz"
mkdir -p "$RUNNER_TEMP/editors/vscode"
tar -xzf "$RUNNER_TEMP/editors/vscode.tar.gz" -C "$RUNNER_TEMP/editors/vscode"
echo "VSCODE_BIN=$RUNNER_TEMP/editors/vscode/VSCode-linux-x64/bin/code" >> "$GITHUB_ENV"
CODIUM_URL="$(curl -fsSL https://api.github.com/repos/VSCodium/vscodium/releases/latest | grep browser_download_url | grep 'VSCodium-linux-x64-.*\.tar\.gz' | head -n1 | cut -d '"' -f4)"
if [[ -z "$CODIUM_URL" ]]; then
echo "Could not determine VSCodium release tarball URL"
exit 1
fi
curl -fL "$CODIUM_URL" -o "$RUNNER_TEMP/editors/vscodium.tar.gz"
mkdir -p "$RUNNER_TEMP/editors/vscodium"
tar -xzf "$RUNNER_TEMP/editors/vscodium.tar.gz" -C "$RUNNER_TEMP/editors/vscodium"
echo "VSCODIUM_BIN=$RUNNER_TEMP/editors/vscodium/bin/codium" >> "$GITHUB_ENV"
- name: Install from VS Code Marketplace in VS Code
shell: bash
run: |
set -euo pipefail
"$VSCODE_BIN" --user-data-dir "$RUNNER_TEMP/vscode-user" --extensions-dir "$RUNNER_TEMP/vscode-exts" --install-extension aleksUIX.vastlint --force
"$VSCODE_BIN" --user-data-dir "$RUNNER_TEMP/vscode-user" --extensions-dir "$RUNNER_TEMP/vscode-exts" --list-extensions | grep -qi '^aleksuix\.vastlint$'
- name: Install from Open VSX in VSCodium
shell: bash
run: |
set -euo pipefail
"$VSCODIUM_BIN" --user-data-dir "$RUNNER_TEMP/codium-user" --extensions-dir "$RUNNER_TEMP/codium-exts" --install-extension aleksUIX.vastlint --force
"$VSCODIUM_BIN" --user-data-dir "$RUNNER_TEMP/codium-user" --extensions-dir "$RUNNER_TEMP/codium-exts" --list-extensions | grep -qi '^aleksuix\.vastlint$'
- name: Install local VSIX in both editors
shell: bash
run: |
set -euo pipefail
VSIX_PATH="$GITHUB_WORKSPACE/vscode/vastlint-smoke.vsix"
test -f "$VSIX_PATH"
"$VSCODE_BIN" --user-data-dir "$RUNNER_TEMP/vscode-user-local" --extensions-dir "$RUNNER_TEMP/vscode-exts-local" --install-extension "$VSIX_PATH" --force
"$VSCODE_BIN" --user-data-dir "$RUNNER_TEMP/vscode-user-local" --extensions-dir "$RUNNER_TEMP/vscode-exts-local" --list-extensions | grep -qi '^aleksuix\.vastlint$'
"$VSCODIUM_BIN" --user-data-dir "$RUNNER_TEMP/codium-user-local" --extensions-dir "$RUNNER_TEMP/codium-exts-local" --install-extension "$VSIX_PATH" --force
"$VSCODIUM_BIN" --user-data-dir "$RUNNER_TEMP/codium-user-local" --extensions-dir "$RUNNER_TEMP/codium-exts-local" --list-extensions | grep -qi '^aleksuix\.vastlint$'