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
1 change: 1 addition & 0 deletions .gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# .gitkeep file auto-generated at 2026-06-10T23:52:39.912Z for PR creation at branch issue-170-9e2e62bb506a for issue https://github.com/link-foundation/command-stream/issues/170
360 changes: 360 additions & 0 deletions docs/case-studies/issue-170/README.md

Large diffs are not rendered by default.

4,504 changes: 4,504 additions & 0 deletions docs/case-studies/issue-170/ci-logs/run-27310950658-failed.log

Large diffs are not rendered by default.

296 changes: 296 additions & 0 deletions docs/case-studies/issue-170/template-workflows/js/example-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
name: Example app

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'examples/universal-app/**'
- 'src/**'
- 'package.json'
- 'package-lock.json'
- 'scripts/update-preview-images.mjs'
- '.github/workflows/example-app.yml'
push:
branches:
- main
paths:
- 'examples/universal-app/**'
- 'src/**'
- 'package.json'
- 'package-lock.json'
- 'scripts/update-preview-images.mjs'
- '.github/workflows/example-app.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
web-build:
name: Build web app
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: npm
cache-dependency-path: examples/universal-app/package-lock.json

- name: Configure GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/configure-pages@v6

- name: Install app dependencies
run: npm ci --prefix examples/universal-app

- name: Build app
run: npm run example:web:build
env:
GITHUB_PAGES: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
VITE_REPOSITORY_URL: https://github.com/${{ github.repository }}

- name: Upload web build artifact
uses: actions/upload-artifact@v7
with:
name: universal-example-web
path: examples/universal-app/dist
if-no-files-found: error

- name: Upload GitHub Pages artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v5
with:
path: examples/universal-app/dist

# Requires Settings → Pages → Source = GitHub Actions to be set once
# in the repository before this job can succeed. See README → "Deploying
# the example app". The Pages source cannot be configured from a workflow.
pages-deploy:
name: Deploy GitHub Pages
needs: [web-build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy Pages artifact
id: deployment
uses: actions/deploy-pages@v5

desktop-package:
name: Package desktop app (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
# Electron Forge packaging currently exits before writing out/ under
# Node 24 in CI. Use the package's supported Node 20 floor here until
# the desktop packaging toolchain is compatible with Node 24.
node-version: '20.x'
cache: npm
cache-dependency-path: examples/universal-app/package-lock.json

- name: Install app dependencies
run: npm ci --prefix examples/universal-app

- name: Package Electron app
shell: bash
run: |
npm run example:desktop:package
for attempt in {1..30}; do
if [[ -d examples/universal-app/out ]] &&
[[ -n "$(find examples/universal-app/out -mindepth 1 -print -quit)" ]]; then
find examples/universal-app/out -maxdepth 2 -mindepth 1 -print
exit 0
fi
sleep 1
done
echo "::error::Desktop package output was not created at examples/universal-app/out"
exit 1
env:
VITE_REPOSITORY_URL: https://github.com/${{ github.repository }}

- name: Upload desktop package
uses: actions/upload-artifact@v7
with:
name: universal-example-desktop-${{ matrix.os }}
path: examples/universal-app/out
if-no-files-found: error

android-build:
name: Build Android app
if: github.event_name == 'workflow_dispatch' && vars.EXAMPLE_APP_ENABLE_ANDROID_BUILD == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: npm
cache-dependency-path: examples/universal-app/package-lock.json

- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Install app dependencies
run: npm ci --prefix examples/universal-app

- name: Add Android project
run: npm --prefix examples/universal-app run mobile:android:add

- name: Build Android project
run: npm --prefix examples/universal-app run mobile:android:build

- name: Upload Android output
uses: actions/upload-artifact@v7
with:
name: universal-example-android
path: |
examples/universal-app/android/app/build/outputs/**/*.apk
examples/universal-app/android/app/build/outputs/**/*.aab
if-no-files-found: warn

ios-build:
name: Build iOS app
if: github.event_name == 'workflow_dispatch' && vars.EXAMPLE_APP_ENABLE_IOS_BUILD == 'true'
runs-on: macos-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: npm
cache-dependency-path: examples/universal-app/package-lock.json

- name: Install app dependencies
run: npm ci --prefix examples/universal-app

- name: Add iOS project
run: npm --prefix examples/universal-app run mobile:ios:add

- name: Build iOS project
run: npm --prefix examples/universal-app run mobile:ios:build

# Regenerate example-app preview screenshots (docs/screenshots/example-app/*)
# using browser-commander + Playwright so README/site images always reflect
# the current UI. Issue: #62. Implementation: scripts/update-preview-images.mjs.
preview-regen:
name: Regenerate Preview Images
runs-on: ubuntu-latest
container:
# Keep this tag in sync with the playwright package version below.
image: mcr.microsoft.com/playwright:v1.59.1-noble
timeout-minutes: 20
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
permissions:
contents: write
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
steps:
- uses: actions/checkout@v6
with:
# Regenerate against main HEAD so the bot commit lands on a
# fast-forward parent regardless of which trigger started the job.
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: Install example app dependencies
run: npm ci --prefix examples/universal-app

- name: Install browser automation dependencies
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
run: npm install --no-save --package-lock=false --no-audit --no-fund browser-commander@0.8.1 playwright@1.59.1

- name: Regenerate preview images
run: node scripts/update-preview-images.mjs

- name: Detect drift
id: drift
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "drift=true" >> "$GITHUB_OUTPUT"
echo "Preview images drifted:"
git status --porcelain
else
echo "drift=false" >> "$GITHUB_OUTPUT"
echo "Preview images already current."
fi

- name: Commit drift back to main
if: steps.drift.outputs.drift == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Stage only generated artifacts so unrelated changes can't leak in.
git add docs/screenshots/example-app/*.png || true
if git diff --cached --quiet; then
echo "::notice::No tracked preview-image changes to commit (drift was outside expected paths)."
git status --porcelain
exit 0
fi
# [skip ci] prevents an infinite re-run loop; the next push to main
# will pick up these fresh images for the regular pages-build.
git commit -m "chore(preview): regenerate example-app preview images [skip ci]"
git push origin HEAD:main

- name: Upload screenshot failure artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: preview-regen-failure-${{ github.run_id }}
path: |
docs/screenshots/
web/test-results/
web/playwright-report/
retention-days: 7
if-no-files-found: ignore

- name: Summarize regeneration result
if: always()
run: |
if [[ "${{ steps.drift.outputs.drift }}" == "true" ]]; then
echo "::notice::Preview images regenerated and (if applicable) committed to main."
else
echo "::notice::Preview images are already up to date."
fi
93 changes: 93 additions & 0 deletions docs/case-studies/issue-170/template-workflows/js/links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Broken Link Checker

on:
push:
branches:
- main
paths:
- '**.md'
- '**.html'
- '.github/workflows/links.yml'
pull_request:
types: [opened, synchronize, reopened]
paths:
- '**.md'
- '**.html'
- '.github/workflows/links.yml'
workflow_dispatch:

# Concurrency: Only one workflow run per branch at a time
# - For main branch: let runs finish so a newer push does not cancel an
# in-flight link check.
# - For PR branches: cancel older runs so new pushes supersede stale checks.
# See: docs/case-studies/issue-25/DETAILED-COMPARISON.md for context
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
link-checker:
name: Check Links
runs-on: ubuntu-latest
# Typical run: <1min with lychee cache. 10min prevents slow
# external hosts or Wayback Machine probes from hanging the workflow.
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v6

- name: Check links with lychee
id: lychee
uses: lycheeverse/lychee-action@v2
with:
# Check all Markdown and HTML files
# Exclude case-studies directory - these are research documents from
# external repos with references to files and issues that don't exist
# in this repository (similar exclusion pattern as eslint.config.js)
args: >-
--verbose
--no-progress
--cache
--max-cache-age 1d
--max-retries 3
--timeout 30
--exclude-path docs/case-studies
'./**/*.md'
'./**/*.html'
# Don't fail the workflow immediately - we want to check web archive first
fail: false
# Output file for broken links report (used by check-web-archive.mjs)
output: lychee/out.md
# Write a job summary
jobSummary: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Check broken links against Web Archive
if: steps.lychee.outputs.exit_code != 0
id: webarchive
run: node scripts/check-web-archive.mjs
env:
LYCHEE_OUTPUT: lychee/out.md

- name: Fail if broken links found and no web archive fallback
if: steps.lychee.outputs.exit_code != 0 && steps.webarchive.outputs.all_archived != 'true'
run: |
echo "::error::Broken links were detected with no Web Archive fallback available."
echo ""
echo "What happened:"
echo " lychee found one or more broken links in the *.md and *.html files of this repository."
echo " The Web Archive (Wayback Machine) check found no archived versions for some of them."
echo ""
echo "How to fix:"
echo " 1. Review the 'Check links with lychee' step above for a full list of broken links."
echo " 2. For links marked with a '::notice::' annotation above, a Web Archive version exists."
echo " Replace those broken links with the suggested archive.org URL."
echo " 3. For links with no archive version, either:"
echo " a. Find an updated URL that points to the same or equivalent content."
echo " b. Remove the link if the content is no longer relevant."
echo " c. Add the URL to .lycheeignore if it is a known false positive."
echo ""
echo "Report location: lychee/out.md (available as a workflow artifact if configured)."
exit 1
Loading
Loading