forked from EXT/VR180-Web-Player
deploy workflow
This commit is contained in:
104
.gitea/workflows/publish-pages.yml
Normal file
104
.gitea/workflows/publish-pages.yml
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Publish Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
R2_ENDPOINT: https://${{ secrets.F40_PAGES_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
||||
R2_ACCOUNT_ID: ${{ secrets.F40_PAGES_R2_ACCOUNT_ID }}
|
||||
R2_BUCKET: ${{ secrets.F40_PAGES_R2_BUCKET }}
|
||||
SITE_NAME_OVERRIDE: ${{ vars.F40_PAGES_SITE_NAME }}
|
||||
BUILD_COMMAND_OVERRIDE: ${{ vars.F40_PAGES_BUILD_COMMAND }}
|
||||
OUTPUT_DIR_OVERRIDE: ${{ vars.F40_PAGES_OUTPUT_DIR }}
|
||||
DEFAULT_BUILD_COMMAND: ${{ vars.F40_PAGES_DEFAULT_BUILD_COMMAND }}
|
||||
DEFAULT_OUTPUT_DIR: ${{ vars.F40_PAGES_DEFAULT_OUTPUT_DIR }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Resolve settings
|
||||
run: |
|
||||
SITE_NAME="${SITE_NAME_OVERRIDE:-${GITHUB_REPOSITORY##*/}}"
|
||||
BUILD_COMMAND="${BUILD_COMMAND_OVERRIDE:-${DEFAULT_BUILD_COMMAND:-npm ci && npm run build:test-app}}"
|
||||
OUTPUT_DIR="${OUTPUT_DIR_OVERRIDE:-${DEFAULT_OUTPUT_DIR:-dist}}"
|
||||
|
||||
echo "SITE_NAME=$SITE_NAME" >> "$GITHUB_ENV"
|
||||
echo "BUILD_COMMAND=$BUILD_COMMAND" >> "$GITHUB_ENV"
|
||||
echo "OUTPUT_DIR=$OUTPUT_DIR" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate publish settings
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.F40_PAGES_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.F40_PAGES_R2_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
missing=0
|
||||
for name in R2_ACCOUNT_ID R2_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||
value="$(eval "printf '%s' \"\${$name:-}\"")"
|
||||
if [ -z "$value" ]; then
|
||||
echo "::error::$name is required"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$SITE_NAME" in
|
||||
""|*[!A-Za-z0-9._-]*)
|
||||
echo "::error::SITE_NAME must contain only letters, numbers, '.', '_', and '-'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
python3 -m pip install --user awscli
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
- name: Build static site
|
||||
run: sh -c "$BUILD_COMMAND"
|
||||
|
||||
- name: Validate build output
|
||||
run: |
|
||||
if [ ! -d "$OUTPUT_DIR" ]; then
|
||||
echo "::error::Build output directory '$OUTPUT_DIR' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$OUTPUT_DIR/index.html" ]; then
|
||||
echo "::warning::'$OUTPUT_DIR/index.html' does not exist; /$SITE_NAME/ will return 404"
|
||||
fi
|
||||
|
||||
- name: Publish to R2
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.F40_PAGES_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.F40_PAGES_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
run: |
|
||||
RUN_ATTEMPT="${GITHUB_RUN_ATTEMPT:-1}"
|
||||
RELEASE="${GITHUB_SHA}-${RUN_ATTEMPT}"
|
||||
PREFIX="sites/${SITE_NAME}/releases/${RELEASE}"
|
||||
PUBLISHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
aws s3 sync "$OUTPUT_DIR/" "s3://${R2_BUCKET}/${PREFIX}/" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--delete \
|
||||
--cache-control "public,max-age=31536000,immutable"
|
||||
|
||||
printf '{"site":"%s","release":"%s","sha":"%s","publishedAt":"%s"}\n' \
|
||||
"$SITE_NAME" "$RELEASE" "$GITHUB_SHA" "$PUBLISHED_AT" > current.json
|
||||
|
||||
aws s3 cp current.json "s3://${R2_BUCKET}/sites/${SITE_NAME}/current.json" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--content-type application/json \
|
||||
--cache-control no-store
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
node_modules/
|
||||
.env.r2
|
||||
dist/
|
||||
|
||||
# Generated by `npm run build`.
|
||||
vr180player/*.css
|
||||
|
||||
38
README.md
38
README.md
@@ -106,6 +106,43 @@ This builds the TypeScript player once, then serves `index.html` with Vite at a
|
||||
|
||||
For headset testing, the page must be a secure context before the browser will expose immersive WebXR. A LAN URL such as `http://192.168.x.x:5173/` is useful for checking layout and media loading, but it will usually not show the headset's immersive VR prompt. Use an HTTPS URL with a trusted certificate, a trusted tunnel, or a deployed CDN/Pages URL for immersive testing.
|
||||
|
||||
## F40 Pages deploy
|
||||
This repo includes a Gitea Actions workflow at `.gitea/workflows/publish-pages.yml`. It builds the test app into `dist/` and uploads it to the shared F40 Pages R2 bucket using the layout:
|
||||
|
||||
```txt
|
||||
sites/{site-name}/releases/{git-sha}-{run-attempt}/
|
||||
sites/{site-name}/current.json
|
||||
```
|
||||
|
||||
By default, this repo uses:
|
||||
|
||||
```sh
|
||||
npm ci && npm run build:test-app
|
||||
```
|
||||
|
||||
The `build:test-app` script compiles the player, copies the generated `vr180player/` CDN assets, copies the simplified `test-pages/` app, and optionally copies `media/` if that local directory exists.
|
||||
|
||||
Required Gitea secrets:
|
||||
|
||||
- `F40_PAGES_R2_ACCOUNT_ID`
|
||||
- `F40_PAGES_R2_BUCKET`
|
||||
- `F40_PAGES_R2_ACCESS_KEY_ID`
|
||||
- `F40_PAGES_R2_SECRET_ACCESS_KEY`
|
||||
|
||||
Optional Gitea variables:
|
||||
|
||||
- `F40_PAGES_SITE_NAME`: defaults to the repository name.
|
||||
- `F40_PAGES_BUILD_COMMAND`: defaults to `npm ci && npm run build:test-app`.
|
||||
- `F40_PAGES_OUTPUT_DIR`: defaults to `dist`.
|
||||
|
||||
When the middleman router serves the current release at `https://pages.f-40.com/{site-name}/`, the generated player can also be used as a CDN from:
|
||||
|
||||
```html
|
||||
<script type="module" src="https://pages.f-40.com/VR-Web-Player/vr180player/vr180-player.js"></script>
|
||||
```
|
||||
|
||||
For stronger CDN caching, the router can expose immutable release URLs and use the `current.json` file only to resolve the latest release.
|
||||
|
||||
## Development
|
||||
The player source is TypeScript in `src/vr180player/`. Generated JavaScript files in `vr180player/` are ignored by git so CI/CD can build and publish them from source.
|
||||
|
||||
@@ -113,6 +150,7 @@ The player source is TypeScript in `src/vr180player/`. Generated JavaScript file
|
||||
npm install
|
||||
npm run dev
|
||||
npm run build
|
||||
npm run build:test-app
|
||||
```
|
||||
|
||||
Edit the TypeScript source files rather than generated JavaScript. A typical CI/CD publish step should run `npm ci`, `npm run build`, then publish `vr180player/` with its generated `.js` files and CSS.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "npm run build && vite --host 0.0.0.0",
|
||||
"build": "tsc && node scripts/copy-styles.mjs",
|
||||
"build:test-app": "npm run build && node scripts/build-test-app.mjs",
|
||||
"check": "tsc --noEmit",
|
||||
"deploy:r2": "npm run build && npm run upload:r2",
|
||||
"test": "npm run build && node --test tests/time.test.mjs tests/projection.test.mjs tests/media-controller.test.mjs tests/texture-manager.test.mjs tests/media-adapter.test.mjs tests/hand-aim.test.mjs tests/input-mode.test.mjs tests/icons.test.mjs tests/control-panel-timing.test.mjs tests/launcher.test.mjs",
|
||||
|
||||
58
scripts/build-test-app.mjs
Normal file
58
scripts/build-test-app.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { cp, mkdir, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const distDir = join(rootDir, 'dist');
|
||||
|
||||
await rm(distDir, { force: true, recursive: true });
|
||||
await mkdir(distDir, { recursive: true });
|
||||
|
||||
await copyRequired('index.html');
|
||||
await copyRequired('poster.jpg');
|
||||
await copyRequired('test-pages');
|
||||
await copyRequired('vr180player');
|
||||
await copyOptional('media');
|
||||
|
||||
await writeFile(
|
||||
join(distDir, '_headers'),
|
||||
[
|
||||
'/vr180player/*',
|
||||
' Cache-Control: public, max-age=3600',
|
||||
'',
|
||||
'/*',
|
||||
' Cache-Control: public, max-age=300',
|
||||
''
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
console.log(`Built test app in ${distDir}`);
|
||||
|
||||
async function copyRequired(relativePath) {
|
||||
const source = join(rootDir, relativePath);
|
||||
const target = join(distDir, relativePath);
|
||||
await cp(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyOptional(relativePath) {
|
||||
const source = join(rootDir, relativePath);
|
||||
if (!await pathExists(source)) {
|
||||
console.warn(`Optional ${relativePath}/ directory not found; bundled sample media will not be included.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await cp(source, join(distDir, relativePath), { recursive: true });
|
||||
}
|
||||
|
||||
async function pathExists(path) {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user