179 lines
8.7 KiB
YAML
179 lines
8.7 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: Finish verified backend registry garbage collection
|
|
run: |
|
|
python3 - <<'PYOPS'
|
|
import subprocess,json,urllib.request,urllib.parse,hashlib,re,time,os
|
|
BASE='https://source.archipelago-foundation.org'
|
|
name='demo-storage-'+os.environ['GITHUB_RUN_ID']
|
|
token=subprocess.check_output(['docker','exec','--user','git','gitea','gitea','--config','/data/gitea/conf/app.ini','admin','user','generate-access-token','--username','lfg2025','--token-name',name,'--scopes','write:admin,write:package','--raw'],text=True).strip().splitlines()[-1]
|
|
assert re.fullmatch('[0-9a-f]{40}',token)
|
|
def request(path,method='GET',auth=None):
|
|
req=urllib.request.Request(BASE+path,method=method,headers={'Authorization':auth or 'token '+token,'Accept':'application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json'})
|
|
with urllib.request.urlopen(req,timeout=60) as r:return r.read()
|
|
def api(path,method='GET'):
|
|
data=request('/api/v1/'+path,method);return json.loads(data) if data else None
|
|
try:
|
|
packages=[]
|
|
for page in range(1,100):
|
|
batch=api(f'packages/lfg2025?type=container&limit=50&page={page}')
|
|
packages.extend(batch)
|
|
if len(batch)<50:break
|
|
else:raise RuntimeError('Pagination did not end')
|
|
live=json.loads(subprocess.check_output(['docker','inspect']+subprocess.check_output(['docker','ps','-aq'],text=True).split(),text=True))
|
|
live_images=[c['Config']['Image'] for c in live]
|
|
print('Preserved running demo images:',[i for i in live_images if 'archy-demo-' in i],flush=True)
|
|
for package in ['archy-demo-backend']:
|
|
versions=[p for p in packages if p['name']==package]
|
|
builds=sorted([p for p in versions if re.fullmatch('[0-9a-f]{40}',p['version'])],key=lambda p:p['created_at'],reverse=True)
|
|
candidates={p['version'] for p in builds[2:] if p['created_at']<'2026-10-01'}
|
|
repo='lfg2025/'+package
|
|
import base64
|
|
basic='Basic '+base64.b64encode(('lfg2025:'+token).encode()).decode()
|
|
bearer=json.loads(request('/v2/token?'+urllib.parse.urlencode({'service':'container_registry','scope':f'repository:{repo}:pull'}),auth=basic))['token']
|
|
auth='Bearer '+bearer
|
|
memo={}
|
|
def tree(ref):
|
|
if ref in memo:return memo[ref]
|
|
raw=request('/v2/'+repo+'/manifests/'+urllib.parse.quote(ref,safe=':'),auth=auth)
|
|
manifest=json.loads(raw);digest='sha256:'+hashlib.sha256(raw).hexdigest();result={digest};memo[ref]=result
|
|
for child in manifest.get('manifests',[]):result.update(tree(child['digest']))
|
|
return result
|
|
protected=set()
|
|
roots=[p['version'] for p in versions if not p['version'].startswith('sha256:') and p['version'] not in candidates]
|
|
prefix=BASE.removeprefix('https://')+'/'+repo
|
|
roots.extend(i.split('@',1)[1] if '@' in i else i[len(prefix)+1:] for i in live_images if i.startswith(prefix+'@') or i.startswith(prefix+':'))
|
|
for root in roots:protected.update(tree(root))
|
|
plan=[]
|
|
for version in sorted(candidates):
|
|
if tree(version)&protected:print('Preserving shared manifest',package,version,flush=True)
|
|
else:plan.append(version)
|
|
print('Verified obsolete builds:',package,plan,flush=True)
|
|
for version in plan:
|
|
api('packages/lfg2025/container/'+package+'/'+version,'DELETE')
|
|
print('Deleted obsolete demo tag',package,version,flush=True)
|
|
# Existing zero-rule guard avoids applying any broad retention policy.
|
|
rules=subprocess.check_output(['docker','exec','gitea','sqlite3','-readonly','/data/gitea/gitea.db','SELECT count(*) FROM package_cleanup_rule WHERE enabled=1;'],text=True).strip()
|
|
assert rules=='0'
|
|
api('admin/cron/cleanup_packages','POST')
|
|
time.sleep(5)
|
|
subprocess.run(['docker','exec','gitea','sh','-c','df -h /data; du -sh /data/gitea/packages'],check=True)
|
|
finally:
|
|
request('/api/v1/token','DELETE')
|
|
print('Temporary maintenance token revoked.',flush=True)
|
|
PYOPS
|
|
- name: Inspect demo compose structure without secrets
|
|
run: |
|
|
python3 - <<'PYOPS'
|
|
import subprocess,yaml,json
|
|
raw=subprocess.check_output(['docker','run','--rm','--entrypoint','cat','--mount','type=bind,src=/opt/archy-demo,dst=/deployment,readonly','gitea/gitea:1.27.1','/deployment/docker-compose.yml'])
|
|
doc=yaml.safe_load(raw)
|
|
for name,service in doc['services'].items():
|
|
print(name,json.dumps({k:service[k] for k in ['image','ports','volumes','networks','env_file','depends_on','container_name'] if k in service}))
|
|
print('environment keys',[key.split('=',1)[0] for key in service.get('environment',{})])
|
|
print('top-level',list(doc))
|
|
PYOPS
|