Files
archy/.gitea/workflows/demo-images.yml
T

176 lines
8.9 KiB
YAML

name: Demo images
# Builds and pushes the public-demo images on every change to the UI / mock
# backend, so the separated `archy-demo` Portainer stack auto-tracks the real
# code (see demo-deploy/ and docs/demo-deployment-design.md).
#
# Required repo configuration:
# vars.DEMO_REGISTRY e.g. source.archipelago-foundation.org/lfg2025
# vars.DEMO_REGISTRY_HOST registry host for docker login (no org suffix)
# secrets.DEMO_REGISTRY_USER
# secrets.DEMO_REGISTRY_TOKEN
# Optional:
# secrets.PORTAINER_WEBHOOK redeploy hook called after a successful push
on:
push:
branches: [main]
paths:
- 'neode-ui/**'
- 'aiui/**'
- 'scripts/build-aiui.sh'
- '.dockerignore'
- 'docker-compose.demo.yml'
- '.gitea/workflows/demo-images.yml'
workflow_dispatch:
inputs:
operation:
description: Build and deploy the demo, or clean disposable runner storage
type: choice
options: [build, storage]
default: build
required: false
jobs:
build:
name: Build & push demo images
runs-on: ubuntu-latest
# Skip cleanly on forks / before registry config is set.
if: ${{ vars.DEMO_REGISTRY != '' && inputs.operation != 'storage' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check runner storage
run: |
df -h /
docker system df
used=$(df -P / | awk 'NR == 2 {gsub(/%/, "", $5); print $5}')
if [ "$used" -ge 85 ]; then
# Only unused cache and untagged images older than a week.
# Running containers, tagged images and volumes are preserved.
docker builder prune --force --filter until=168h --keep-storage 10GB
docker image prune --force --filter until=168h
df -h /
docker system df
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# Use the public HTTPS registry, including for BuildKit pushes.
# Plain HTTP redirects can change upload POST requests into GET requests.
buildkitd-config-inline: |
[registry."${{ vars.DEMO_REGISTRY_HOST || vars.DEMO_REGISTRY }}"]
http = false
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DEMO_REGISTRY_HOST || vars.DEMO_REGISTRY }}
username: ${{ secrets.DEMO_REGISTRY_USER }}
password: ${{ secrets.DEMO_REGISTRY_TOKEN }}
- name: Build & push backend
uses: docker/build-push-action@v6
with:
context: .
file: neode-ui/Dockerfile.backend
push: true
tags: |
${{ vars.DEMO_REGISTRY }}/archy-demo-backend:demo
${{ vars.DEMO_REGISTRY }}/archy-demo-backend:${{ github.sha }}
- name: Build & push web
uses: docker/build-push-action@v6
with:
context: .
file: neode-ui/Dockerfile.web
push: true
build-args: |
VITE_DEMO=1
SOURCE_REVISION=${{ github.sha }}
tags: |
${{ vars.DEMO_REGISTRY }}/archy-demo-web:demo
${{ vars.DEMO_REGISTRY }}/archy-demo-web:${{ github.sha }}
- name: Trigger Portainer redeploy
# Source pushes prepare images; public deployment is an explicit post-release action.
if: ${{ success() && github.event_name == 'workflow_dispatch' && secrets.PORTAINER_WEBHOOK != '' }}
run: curl -fsS -X POST "${{ secrets.PORTAINER_WEBHOOK }}"
storage:
if: ${{ github.event_name == 'workflow_dispatch' && inputs.operation == 'storage' }}
runs-on: ubuntu-latest
steps:
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DEMO_REGISTRY_HOST || vars.DEMO_REGISTRY }}
username: ${{ secrets.DEMO_REGISTRY_USER }}
password: ${{ secrets.DEMO_REGISTRY_TOKEN }}
- name: Deploy verified viewport build with rollback
run: |
python3 - <<'PYOPS'
import subprocess,json,re,pathlib,time,urllib.request
REV='c66689ab61cd188885468808703a14df2114889e'
ROOT=pathlib.Path('/opt/archy-demo');ROOT.mkdir(parents=True,exist_ok=True)
HELPER=['docker','run','--rm','--interactive','--entrypoint','sh','--mount','type=bind,src=/opt/archy-demo,dst=/deployment','gitea/gitea:1.27.1']
def run(args,**kw):return subprocess.run(args,check=True,**kw)
def capture(args):return subprocess.check_output(args,text=True)
original=capture(HELPER+['-ec','cat /deployment/docker-compose.yml'])
env=capture(HELPER+['-ec','if [ -f /deployment/.env ]; then cat /deployment/.env; fi'])
base=ROOT/'docker-compose.yml';base.write_text(original);base.chmod(0o600)
(ROOT/'.env').write_text(env);(ROOT/'.env').chmod(0o600)
compose=['docker','compose','--project-directory',str(ROOT),'-p','archipelago-demo','-f',str(base)]
config=json.loads(capture(compose+['config','--format','json']))
assert set(config['services'])=={'neode-web','neode-backend'},'Unexpected services; stop before deployment'
# Do not allow old host static files to shadow the reviewed image.
for service in config['services'].values():
for volume in service.get('volumes',[]):
assert not volume['target'].startswith('/usr/share/nginx/html') or (volume['target']=='/usr/share/nginx/html/packages' and volume.get('source')=='/opt/archy-demo/companion-packages' and volume.get('read_only') is True),'Unexpected static override requires review'
images={}
for role in ['backend','web']:
image='source.archipelago-foundation.org/chaum/archy-demo-'+role+':'+REV
run(['docker','pull',image])
info=json.loads(capture(['docker','image','inspect',image]))[0]
images['neode-'+role]=next(d for d in info['RepoDigests'] if d.startswith('source.archipelago-foundation.org/chaum/archy-demo-'+role+'@'))
web=config['services']['neode-web']
assert not any(v['target']=='/etc/nginx/nginx.conf' for v in web.get('volumes',[])), 'Unexpected legacy nginx override'
current=None;count=0;lines=[]
for line in original.splitlines(keepends=True):
match=re.match(r'^ ([A-Za-z][\w-]*):\s*$',line)
if match:current=match.group(1)
if current in images and re.match(r'^ image:',line):
line=' image: '+images[current]+'\n';count+=1
lines.append(line)
assert count==2,'Expected exactly two demo image replacements'
updated=''.join(lines);base.write_text(updated)
run(compose+['config','--quiet'])
# Tag running images before replacement, including the old web digest missing from registry.
for role in ['web','backend']:
image_id=capture(['docker','inspect','archy-demo-'+role,'--format','{{.Image}}']).strip()
run(['docker','image','tag',image_id,'archy-demo-'+role+':rollback-before-settle-20261112'])
backup='docker-compose.before-settle-20261112.yml'
run(HELPER+['-ec',f'umask 077; if [ ! -e /deployment/{backup} ]; then cp /deployment/docker-compose.yml /deployment/{backup}; fi; cat > /deployment/docker-compose.yml.next; test -s /deployment/docker-compose.yml.next; mv /deployment/docker-compose.yml.next /deployment/docker-compose.yml'],input=updated,text=True)
try:
run(compose+['up','-d','--no-build','--pull','never','--wait','--wait-timeout','120','neode-backend','neode-web'])
time.sleep(3)
receipt=capture(['docker','exec','archy-demo-web','cat','/usr/share/nginx/html/aiui/BUILD-INFO'])
assert 'commit='+REV in receipt,receipt
with urllib.request.urlopen('https://demo.archipelago-foundation.org/aiui/BUILD-INFO?revision='+REV,timeout=30) as r:public=r.read().decode()
assert 'commit='+REV in public,public
print('Deployed reviewed demo:',REV,flush=True)
print('Pinned images:',json.dumps(images),flush=True)
print('Rollback compose:',str(ROOT/backup),flush=True)
except Exception:
diagnostic=subprocess.run(['docker','logs','--tail','12','archy-demo-web'],capture_output=True,text=True,timeout=10)
print(re.sub(r'sk-[A-Za-z0-9_-]+','[redacted]',diagnostic.stderr),flush=True)
run(HELPER+['-ec',f'cp /deployment/{backup} /deployment/docker-compose.yml'])
base.write_text(original)
override=ROOT/'rollback-images.json';override.write_text(json.dumps({'services':{'neode-'+role:{'image':'archy-demo-'+role+':rollback-before-settle-20261112'} for role in ['web','backend']}}))
run(compose+['-f',str(override),'up','-d','--no-build','--pull','never','--wait','--wait-timeout','120','neode-backend','neode-web'])
print('Deployment check failed; previous demo restored.',flush=True)
raise
PYOPS