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
39 changes: 39 additions & 0 deletions .github/workflows/ci-gc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI GC (stale PR stacks)

on:
schedule:
- cron: '0 6 * * *' # daily 06:00 UTC
workflow_dispatch:
inputs:
max_age_hours:
description: 'Tear down PR stacks older than this many hours'
type: number
default: 24

permissions:
id-token: write
contents: read

jobs:
gc:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Install SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.OPENCOMPUTER_CI_SSH_KEY }}" > ~/.ssh/opencomputer-ci
chmod 600 ~/.ssh/opencomputer-ci

- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: GC stale PR stacks
env:
MAX_AGE_HOURS: ${{ inputs.max_age_hours || 24 }}
run: ./ci/persistent/gc.sh
94 changes: 94 additions & 0 deletions .github/workflows/pr-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: PR Integration Test

on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]

concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
id-token: write
contents: read
pull-requests: write

jobs:
gate:
runs-on: ubuntu-latest
outputs:
approved: ${{ steps.check.outputs.approved }}
reason: ${{ steps.check.outputs.reason }}
steps:
- id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AUTHOR: ${{ github.event.pull_request.user.login }}
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: |
if echo "$LABELS" | grep -q '"osb-ci-approve"'; then
echo "approved=true" >> "$GITHUB_OUTPUT"
echo "reason=labeled (osb-ci-approve)" >> "$GITHUB_OUTPUT"
exit 0
fi
if gh api "orgs/diggerhq/members/$AUTHOR" --silent 2>/dev/null; then
echo "approved=true" >> "$GITHUB_OUTPUT"
echo "reason=org-member" >> "$GITHUB_OUTPUT"
else
echo "approved=false" >> "$GITHUB_OUTPUT"
echo "reason=outside-contributor; maintainer must apply osb-ci-approve label to run CI" >> "$GITHUB_OUTPUT"
fi

- name: Comment on skip
if: steps.check.outputs.approved != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr comment ${{ github.event.pull_request.number }} -R ${{ github.repository }} \
--body "CI skipped: ${{ steps.check.outputs.reason }}"

ci:
needs: gate
if: needs.gate.outputs.approved == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.OPENCOMPUTER_CI_SSH_KEY }}" > ~/.ssh/opencomputer-ci
chmod 600 ~/.ssh/opencomputer-ci
# Public half is derived once from the private key.
ssh-keygen -y -f ~/.ssh/opencomputer-ci > ~/.ssh/opencomputer-ci.pub

- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Bring up PR stack
id: up
run: |
./ci/pr/up.sh ${{ github.event.pull_request.number }} 1

- name: API test suite
env:
OSB_TEST_SERVER_URL: ${{ steps.up.outputs.server_url }}
OSB_TEST_API_KEY: ${{ steps.up.outputs.api_key }}
OSB_TEST_WORKERS: ${{ steps.up.outputs.workers }}
run: go test -count=1 -v ./ci/tests/api/...

- name: Tear down PR stack
if: always()
run: |
./ci/pr/down.sh ${{ github.event.pull_request.number }} || true
23 changes: 23 additions & 0 deletions ci/persistent/down.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Tear down the entire opencomputer-ci RG. Big red button — only for full reset.
# Storage account contents (compat-corpus + checkpoints) and KV secrets are
# unrecoverable after this runs (KV soft-delete retains for 90 days; storage
# is gone). Confirm before invoking.

set -euo pipefail

RG="${RG:-opencomputer-ci}"

if ! az group exists --name "$RG" 2>/dev/null | grep -q true; then
echo "RG $RG doesn't exist; nothing to do."
exit 0
fi

echo ">>> About to delete EVERYTHING in resource group: $RG"
echo " (KV: opencomputer-ci-kv goes into 90-day soft-delete)"
echo " (Storage: data is permanently lost)"
read -r -p "Type 'yes' to proceed: " confirm
[[ "$confirm" == "yes" ]] || { echo "aborted."; exit 1; }

az group delete -n "$RG" --yes --no-wait
echo "delete initiated (running in background, takes ~5 min to complete)."
62 changes: 62 additions & 0 deletions ci/persistent/gc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Garbage-collect stale PR stacks (older than MAX_AGE_HOURS, default 24).
#
# Lists pr-*-server VMs, checks their creation time, and tears down any older
# than the threshold. Designed to run from a GitHub Actions scheduled workflow
# (daily). Safe to re-run; idempotent.
#
# Usage: ./gc.sh # default: stacks >24h old
# MAX_AGE_HOURS=4 ./gc.sh # tighter window

set -euo pipefail

RG="${RG:-opencomputer-ci}"
MAX_AGE_HOURS="${MAX_AGE_HOURS:-24}"
DOWN_SCRIPT="${DOWN_SCRIPT:-$(dirname "$0")/../pr/down.sh}"

[[ -x "$DOWN_SCRIPT" ]] || { echo "FATAL: $DOWN_SCRIPT not executable"; exit 1; }

echo ">>> scanning pr-*-server VMs in $RG (cutoff: ${MAX_AGE_HOURS}h)"

cutoff_epoch=$(($(date -u +%s) - MAX_AGE_HOURS * 3600))

# VM names don't contain whitespace, so simple word-splitting works (and is
# portable to macOS bash 3.2, which lacks mapfile).
VMS=$(az vm list -g "$RG" --query "[?starts_with(name,'pr-') && ends_with(name,'-server')].name" -o tsv)
if [[ -z "$VMS" ]]; then
echo "no PR stacks found."
exit 0
fi

KILLED=0
for vm in $VMS; do
created=$(az vm show -g "$RG" -n "$vm" --query "timeCreated" -o tsv 2>/dev/null || echo "")
if [[ -z "$created" ]]; then
echo " $vm: no timeCreated metadata, skipping"
continue
fi

# Strip fractional seconds + parse to epoch (BSD/macOS and Linux tolerant).
ts=$(echo "$created" | sed -E 's/\.[0-9]+//; s/\+.*//')
if date --version >/dev/null 2>&1; then
created_epoch=$(date -u -d "$ts" +%s)
else
created_epoch=$(date -u -j -f "%Y-%m-%dT%H:%M:%S" "$ts" +%s)
fi

age_h=$(( ($(date -u +%s) - created_epoch) / 3600 ))
pr_num=$(echo "$vm" | sed -E 's/^pr-([0-9]+)-server$/\1/')

if [[ "$created_epoch" -lt "$cutoff_epoch" ]]; then
echo " $vm (PR $pr_num, age=${age_h}h) — TEARING DOWN"
if "$DOWN_SCRIPT" "$pr_num"; then
KILLED=$((KILLED + 1))
else
echo " down.sh failed for PR $pr_num — continuing"
fi
else
echo " $vm (PR $pr_num, age=${age_h}h) — keeping"
fi
done

echo "DONE. tore down $KILLED stale stack(s)."
Loading