Skip to content
Draft
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
79 changes: 79 additions & 0 deletions .github/workflows/profile-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Approved website profile proposal

'on':
repository_dispatch:
types: [vr-lab-profile-approved-v1]
pull_request:
branches: [main]
workflow_dispatch: {}

permissions: {}

jobs:
synthetic-tests:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Install pinned receiver dependencies
run: python -m pip install --disable-pip-version-check -r scripts/profile_sync/requirements.txt
- name: Run synthetic and failure-path tests
run: python -B -m unittest discover -s scripts/profile_sync -p 'test_profile_sync.py' -v
- name: Validate disabled Power Automate definition
run: python -B scripts/profile_sync/powerautomate/test_powerautomate_template.py
- name: Validate disabled Microsoft-only manual handoff
run: python -B scripts/profile_sync/powerautomate/test_powerautomate_manual_handoff.py

propose-profile:
needs: synthetic-tests
if: >-
github.event_name == 'repository_dispatch' &&
github.repository == 'maxdiluca/virtualrealitylab' &&
github.ref == 'refs/heads/main' &&
github.event.sender.login == 'maxdiluca' &&
vars.PROFILE_SYNC_ENABLED == 'true'
environment: website-profile-review
runs-on: ubuntu-24.04
timeout-minutes: 15
concurrency:
group: website-profile-sync-write
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Install pinned receiver dependencies
run: python -m pip install --disable-pip-version-check -r scripts/profile_sync/requirements.txt
- name: Install verified official extended Hugo
shell: bash
run: |
set -euo pipefail
install -d "$RUNNER_TEMP/profile-sync-hugo"
curl --fail --location --max-time 60 --silent --show-error --output "$RUNNER_TEMP/profile-sync-hugo.tar.gz" https://github.com/gohugoio/hugo/releases/download/v0.152.2/hugo_extended_0.152.2_linux-amd64.tar.gz
printf '%s %s\n' 416bcfbdf5f68469ec9644dbe507da50fc21b94b69a125b059d64ed2cb4d8c27 "$RUNNER_TEMP/profile-sync-hugo.tar.gz" | sha256sum --check --strict
tar -xzf "$RUNNER_TEMP/profile-sync-hugo.tar.gz" -C "$RUNNER_TEMP/profile-sync-hugo" hugo
printf '%s\n' "$RUNNER_TEMP/profile-sync-hugo" >> "$GITHUB_PATH"
- name: Validate exact approved bytes, build privately, and reconcile draft proposal
env:
PROFILE_SYNC_ENABLED: ${{ vars.PROFILE_SYNC_ENABLED }}
PROFILE_SYNC_REPOSITORY: maxdiluca/virtualrealitylab
PROFILE_SYNC_BASE_BRANCH: main
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
python -B scripts/profile_sync/profile_sync.py --event-json "$GITHUB_EVENT_PATH" --repo "$GITHUB_WORKSPACE" --output "$RUNNER_TEMP/profile-sync-stage-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" --publish
28 changes: 28 additions & 0 deletions config/_default/hugo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,31 @@ security:
_merge: deep
sitemap:
_merge: deep

# Keep the existing file-backed profiles as the sole rendered owners of these
# author URLs. These verified generated terms retain canonical links.
cascade:
- _target:
kind: term
path: /authors/william-smith
build:
render: link
list: never
- _target:
kind: term
path: /authors/kika-okwubuasi-andrew
build:
render: link
list: never
- _target:
kind: term
path: /authors/youngbin-park
build:
render: link
list: never
- _target:
kind: term
path: /authors/melissa-mira-zulle
build:
render: link
list: never
10 changes: 10 additions & 0 deletions config/_default/module.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
############################

imports:
- path: github.com/HugoBlox/hugo-blox-builder/modules/blox-seo
mounts:
- source: assets
target: assets
- source: layouts
target: layouts
excludeFiles:
- _default/_markup/sitemap.xml
- source: layouts/_default/_markup/sitemap.xml
target: layouts/sitemap.xml
- path: github.com/HugoBlox/hugo-blox-builder/modules/blox-plugin-decap-cms
- path: github.com/HugoBlox/hugo-blox-builder/modules/blox-plugin-netlify
- path: github.com/HugoBlox/hugo-blox-builder/modules/blox-bootstrap/v5
4 changes: 2 additions & 2 deletions content/site_maintenance/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ type: maintenance

slug:
private: false
_build:
list: true
build:
list: always

---
<meta http-equiv="refresh" content="5">
135 changes: 135 additions & 0 deletions scripts/profile_sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Approved website profile receiver

Status: staged implementation, disabled by default. This package is designed for
`maxdiluca/virtualrealitylab`, base branch `main`, and authentic GitHub
`repository_dispatch` events whose `sender.login` is `maxdiluca`. Installing this
code or passing tests does not approve publication, connect Power Automate, or
establish a live service.

A separate [Microsoft-only manual-handoff option](powerautomate/manual-handoff.md)
keeps GitHub upload as an operator task. It preserves frozen approval snapshots
and stops at `approved-awaiting-manual-validation`; its cloud actions and optional
handoff mail remain disabled. The local recipe validates both downloaded files
and runs the receiver's strict private Hugo preview before any public upload.
Native installation and a separately authorized synthetic pilot are pending.

Deploy this directory's Python, requirements, tests, fixtures, README and
`powerautomate/` instructions under `scripts/profile_sync/` in the website
repository. Deploy `profile-sync.yml` as `.github/workflows/profile-sync.yml`.
Do not merge or enable without the website owner's exact-version review.

Use CPython 3.12 and the pinned dependencies:

```bash
python -m pip install -r scripts/profile_sync/requirements.txt
python -B -m unittest discover -s scripts/profile_sync -p 'test_profile_sync.py' -v
python -B scripts/profile_sync/powerautomate/test_powerautomate_template.py
python -B scripts/profile_sync/powerautomate/test_powerautomate_manual_handoff.py
python -B scripts/profile_sync/profile_sync.py --event-json /private/path/event.json --repo /private/path/website-checkout --output /private/path/new-staging-directory
```

Local staging writes two approved files and a hash receipt in a new private
directory; it performs no network calls. Payload Markdown/image content and
person fields are never printed. Do not use real source records in this public
package or its fixtures. The sole fixture is synthetic.

The event action is `vr-lab-profile-approved-v1`. `client_payload` has exactly
these six keys:

```json
{
"schema_version": 1,
"request_id": "opaque-synthetic-reserved-key",
"folder": "Synthetic_Member",
"profile_markdown": "---\n...\n---\n\nApproved biography text...",
"avatar": {"filename": "selected.png", "content_base64": "..."},
"approval": {"profile_approved": true, "image_approved": true}
}
```

Folder names start with an ASCII letter and contain at most 80 ASCII letters,
digits, underscores or hyphens. Request IDs contain at most 100 such characters
and start with a letter or digit. The private sender ledger reserves each
request ID for one immutable approved snapshot; the receiver never treats a
payload assertion as proof of real consent. The configured connection and
protected GitHub environment must establish that provenance.

The entire compact UTF-8 payload is limited to 60,000 bytes. Markdown is limited
to 16,000 UTF-8 bytes and the decoded image to 32 KiB. Supported front matter is
`title`, `authors`, `superuser`, `role`, `organizations`, `user_groups`, and
optional `interests`/`social`. The author list must equal the exact folder name;
`superuser` must be false and groups must match the site's explicit group list.
The initial generated skeleton is incomplete: a useful biography is required.
This first release accepts prose, lists and emphasis; raw HTML, Hugo shortcodes,
Markdown links/code, YAML aliases/tags and unknown fields require human review.
Public links must be HTTPS and belong in validated organization/social fields.

Approval covers the exact Markdown and image bytes, including the announced
path changes: `index.md` becomes `_index.md`, and the selected picture becomes
`avatar.png` or `avatar.jpg`. Existing `_index.md` or `index.md` names are
preserved during duplicate comparison. The receiver does not normalize YAML,
remove null placeholders, rotate, crop or reencode images after approval.
Pillow verifies and fully decodes images. PNG accepts only required pixel and
transparency chunks; JPEG accepts basic/progressive image markers, basic JFIF
without embedded thumbnails and fixed Adobe color information. EXIF, XMP,
comments, ICC/unknown metadata, animation, oversized dimensions and trailing
data are withheld. Clean such an image privately and obtain new approval of
the cleaned file before submitting it.

This release adds new profiles only. Existing author directories must contain
exactly the same two files and bytes for a no-op; case collisions, extra files
or changed content require human review. One deterministic branch,
`codex/profile-sync/<lowercase-folder>`, holds a pending proposal. A retry reads
and verifies that branch's one-commit, two-added-file changes and exact bytes
before creating/reconciling its draft pull request. An edited branch or closed
proposal is a stopping condition; no force-push, overwrite, delete or auto-merge
is implemented.

Publishing additionally requires `--publish`, `PROFILE_SYNC_ENABLED=true`, the
fixed repository/base environment bindings, an authentic dispatch context and
`GITHUB_TOKEN`. Checkout HEAD must match the event SHA and current remote main.
Before any mutation, the receiver uses Git's tracked-file list to copy only
the website's `assets/`, `config/`, `content/`, `layouts/` and `static/` source,
plus exactly `go.mod`, `go.sum` and `theme.toml`, to private temporary directories.
It filters unrelated tracked names before inspecting or copying their files;
operational outputs, root mail, local settings and generated artifacts stay
outside the snapshot. Symlinks inside the selected source are rejected and the
selected bytes remain limited to 250 MiB. Unknown build dependencies must fail
the real baseline build rather than silently broadening this allowlist. It uses exact
extended Hugo 0.152.2 for baseline and proposed builds with
`--printPathWarnings --panicOnWarning`. The proposed build must add exactly one
author HTML route. Build tools receive a minimal environment without GitHub
tokens; build output is captured privately. No Python approximation of Hugo's
title/slug rules is used. A missing tool, existing warning, route collision,
missing rendered author page or changed base stops the write.

The workflow installs the official extended Linux Hugo archive only after
checking its fixed SHA-256. It uses immutable GitHub Action commit pins, runs
synthetic tests with read-only permissions, and limits the write job to the
trusted dispatch on main. Setup must create the `website-profile-review`
environment with an independent required reviewer, prevent self-approval,
restrict it to main, retain branch protections, and permit GitHub Actions to
create pull requests. Enable the repository variable only after a private
synthetic full-site build and a controlled draft-PR/read-back exercise. A missing
environment protection cannot be inferred from its name and must be checked.

This is a cloud workflow invoked by Power Automate, not a local poller. It does
not merge, deploy, change existing profiles, prove institutional authority,
clean source folders, alert people, or verify a live deployed page. GitHub may
require explicit follow-up checks/workflows for PRs created by `GITHUB_TOKEN`;
do not assume a draft PR triggered every existing CI or deploy workflow.
Publication remains a separate owner-reviewed merge followed by a check of the
deployed page and selected image. Never broaden the sender or field allowlists
to work around a withheld submission.

Full-site validation on 2 October 2026 passed against website base
`f7be0695c2061e249deee020167778fed5e55ca0` plus the three proposed site repairs:
mount the pinned `blox-seo` sitemap at its proper layout path, replace deprecated
maintenance-page build options, and stop four generated taxonomy terms from
rendering over their existing file-backed profiles. The existing canonical
author URLs are retained. The actual receiver's strict baseline and synthetic
proposal builds passed with no warnings; author HTML routes changed from 141
to 142, adding only `/author/synthetic-member/`. No warning gate was relaxed.
These repairs remain a code proposal until the website owner reviews and merges
the pull request. Local build success does not establish cloud execution,
profile consent or deployment.
16 changes: 16 additions & 0 deletions scripts/profile_sync/fixtures/approved-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"action": "vr-lab-profile-approved-v1",
"sender": {"login": "maxdiluca"},
"repository": {"full_name": "maxdiluca/virtualrealitylab"},
"client_payload": {
"schema_version": 1,
"request_id": "synthetic-only-20261002-001",
"folder": "Synthetic_Member",
"profile_markdown": "---\ntitle: Synthetic Member\nauthors: [Synthetic_Member]\nsuperuser: false\nrole: Synthetic Project Student\norganizations:\n - name: University of Birmingham\n url: https://www.birmingham.ac.uk\ninterests: [null]\nsocial:\n - icon: link\n icon_pack: fas\n link: null\nuser_groups: [Project Students]\n---\n\nThis entirely synthetic member explores how people perceive virtual environments. This biography contains no real person data.\n",
"avatar": {
"filename": "synthetic.png",
"content_base64": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAI0lEQVR4nGOUC6hgIAUwkaSaYVQDcYCJSHVwMKqBGEByKAEA8mgBBjzodkQAAAAASUVORK5CYII="
},
"approval": {"profile_approved": true, "image_approved": true}
}
}
Loading