From 0b9473bcb67f2966d976afe4771c8f488a38866d Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Thu, 23 Jul 2026 12:38:32 +0100 Subject: [PATCH 01/14] Add sync workflows --- .../workflows/studios-auto-tag-release.yml | 45 ++++++++++++ .../workflows/studios-upstream-sync-pr.yml | 70 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 .github/workflows/studios-auto-tag-release.yml create mode 100644 .github/workflows/studios-upstream-sync-pr.yml diff --git a/.github/workflows/studios-auto-tag-release.yml b/.github/workflows/studios-auto-tag-release.yml new file mode 100644 index 00000000000..e100d4b31f9 --- /dev/null +++ b/.github/workflows/studios-auto-tag-release.yml @@ -0,0 +1,45 @@ +name: Auto Tag Merged Sync PR + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + tag-release: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check Merge Commit & Create Tag + run: | + # Get the last commit message on main + COMMIT_MSG=$(git log -1 --pretty=%B) + + # Regex check if the commit came from a sync/vX.Y.Z branch merge + if [[ "$COMMIT_MSG" =~ sync/(v[0-9]+\.[0-9]+\.[0-9]+) ]]; then + UPSTREAM_VERSION="${BASH_REMATCH[1]}" + NEW_TAG="itv-$UPSTREAM_VERSION" + + echo "Detected merge for version $UPSTREAM_VERSION." + + # Check if tag already exists + if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then + echo "Tag $NEW_TAG already exists. Skipping." + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + echo "Creating tag $NEW_TAG..." + git tag "$NEW_TAG" + git push origin "$NEW_TAG" + fi + else + echo "Commit is not a sync branch merge. Skipping tag creation." + fi \ No newline at end of file diff --git a/.github/workflows/studios-upstream-sync-pr.yml b/.github/workflows/studios-upstream-sync-pr.yml new file mode 100644 index 00000000000..d3589c97a2e --- /dev/null +++ b/.github/workflows/studios-upstream-sync-pr.yml @@ -0,0 +1,70 @@ +name: Auto Sync Upstream Releases + +on: + schedule: + - cron: '0 6 * * *' # Runs daily at 06:00 UTC + workflow_dispatch: # Allows manual trigger anytime from Actions tab + +permissions: + contents: write + pull-requests: write + +jobs: + sync-upstream: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch Upstream Tags & Create Sync PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + UPSTREAM_URL: "https://github.com/open-webui/open-webui.git" + run: | + # 1. Setup upstream remote to fetch ONLY tags + git remote add upstream "$UPSTREAM_URL" + git config remote.upstream.fetch "+refs/tags/*:refs/tags/*" + git fetch upstream + + # 2. Get latest upstream tag matching version pattern (e.g., v1.2.0) + LATEST_TAG=$(git tag -l "v*" | sort -V | tail -n 1) + + if [ -z "$LATEST_TAG" ]; then + echo "No matching upstream tags found." + exit 0 + fi + + echo "Latest upstream tag: $LATEST_TAG" + + MY_TAG="itv-$LATEST_TAG" + BRANCH_NAME="sync/$LATEST_TAG" + + # 3. Skip if we already deployed this tag or if a sync branch/PR exists + if git rev-parse "$MY_TAG" >/dev/null 2>&1; then + echo "Tag $MY_TAG already exists in fork. Skipping." + exit 0 + fi + + if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then + echo "Branch $BRANCH_NAME already exists on remote. Skipping." + exit 0 + fi + + # 4. Create sync branch off main and merge tag + git checkout -b "$BRANCH_NAME" main + git merge "$LATEST_TAG" -m "sync: merge upstream $LATEST_TAG" + git push origin "$BRANCH_NAME" + + # 5. Open Pull Request via GitHub CLI + gh pr create \ + --title "sync: upstream release $LATEST_TAG" \ + --body "Automated PR bringing upstream release \`$LATEST_TAG\` into \`main\`." \ + --head "$BRANCH_NAME" \ + --base "main" \ No newline at end of file From d05a9173bf88ddf03538a582d746a9651d33177f Mon Sep 17 00:00:00 2001 From: Rob Kendal <38687284+bpk68@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:25:53 +0100 Subject: [PATCH 02/14] Add repository structure and updating strategy documentation --- docs/adr/repo-structure.md | 146 +++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/adr/repo-structure.md diff --git a/docs/adr/repo-structure.md b/docs/adr/repo-structure.md new file mode 100644 index 00000000000..f15c79d8599 --- /dev/null +++ b/docs/adr/repo-structure.md @@ -0,0 +1,146 @@ +# ITV AI Agent Hub - OpenWebUI (OWUI) repo, structure, and updating strategy + +## Architecture decisions and recommendations + +## 1. Purpose and scope + +This document captures the key decisions for ITV Studios Agent Hub, specifically around its primary chosen interface platform, Open WebUI (OWUI). It follows on from a successful MVP programme also built using OWUI, and covers the approach for the production build: how we adopt and maintain OWUI, how we govern model access, and how our own AI agents are exposed to users. + +Decisions here reflect the engineering team’s current thinking and should be revisited as the platform matures and OWUI itself evolves. + +## 2. Licensing + +OWUI moved from a permissive BSD-3-Clause licence to a custom 'Open WebUI Licence' at version 0.6.6 around April 2025. This licence retains permissive use and modification rights, but adds a branding protection clause: + +> “deployments with more than 50 aggregate end users in any rolling 30-day period may not remove or alter the 'OWUI' name, logo, or UI marks unless they hold an Enterprise licence or specific written permission.” + +### Decision + +ITV’s plan is to purchase an OWUI Enterprise licence. This removes the branding restriction entirely and allows for full white-labelling. Enterprise licensing also offers: + +- SLA-backed commercial support +- Access to Long-Term Support (LTS) release versions +- A supported path for full theming and custom branding without legal ambiguity + +This decision removes licensing as a constraint on the technical approach to how we customise and brand the OWUI interface described in the rest of this document. + +## 3. Repository forking and upstream sync strategy + +The core OWUI product is a living codebase; an ongoing concern in active development and maintenance. This means we can benefit from OWUI’s engineering team producing new features, pushing bug fixes, and closing security vulnerabilities. Whilst this is an absolute win for the business, it creates a challenge for the Agent Hub Delivery Team in bringing these changes into our product from the upstream codebase. + +There are two approaches to take: + +1. We pull a fork of the original OWUI project and track it as an upstream remote and merge in new tagged releases as they become available. +2. We create a brand new repository based on the latest version of OWUI and manually merge in updates and releases as we diverge away from the original codebase. + +### Decision + +The recommendation – based on several discussions with businesses in similar positions, documentation and agentic-supported research – is that ITV Studios Agent Hub should is **built as a maintained fork** of `open-webui/open-webui`, rather than a from-scratch build, in order to benefit from OWUI's ongoing feature development, security fixes, and community ecosystem with minimal ongoing engineering cost. + +In practice this means: + +- Fork `open-webui/open-webui` into ITV's own GitHub organisation. +- Add the original repository as a git remote (upstream) and track tagged releases only — never the moving `main` or `dev` branches. +- Pull in new upstream releases via a short-lived `sync/vX.Y.Z` branch, + - Apply changes via merging (**not rebasing**) into main. + - Review changes via standard PR process. + - Once deployed, tag with appropriate release version, e.g. `itv-vX.Y.Z`. +- Keep ITV-specific changes confined to configuration, branding assets, and deployment tooling — avoid modifying OWUI's own source files wherever possible, to keep merge conflicts rare and upstream syncs low-risk. + +### Upstream open-source contributions + +A path exists to contribute fixes back upstream to the main OWUI project where useful. Any contribution merged after v0.6.5 requires agreeing to OWUI's Contributor Licence Agreement (CLA), and the core team heavily reviews and reworks community PRs, so this is best reserved for genuinely general-purpose fixes rather than ITV-specific work. + +## 4. Customisation approach + +OWUI is layered in a way that supports the Agent Hub Delivery Team’s goal of staying easy to update and keep in sync with upstream releases. + +The proposed approach below favours the lowest-friction customisation layer available for each need: + +| Layer | Use for | +|------ | --------| +|**CSS theming**
(custom.css, injected via our own Docker image layer) | Branding, colours, fonts, ITV visual identity — no core source changes required | +| **Functions, Tools, Filters**
(in-process Python plugins, managed via Admin Panel) | Lightweight behavioural changes: message filters, custom UI buttons, new model source wiring | +| **Pipelines**
(standalone microservice, OpenAI-API-compatible) | Heavier processing that shouldn't run inside the OWUI process itself | +| **Direct source modification** | Avoided wherever possible — reserved for cases with no other option, since this is where upstream merge conflicts arise | + +It’s recommended that we minimise any direct customisation of OWUI’s existing codebase to reduce treacherous version updates. Modifications should be created as separate modules or Svelte components and brought into their relevant areas in as discreet a manner as possible. For example, adding custom buttons to the chat window or specialist HTML responses. + +Given our own AI agents will live in a separate Python codebase/repository, the strong preference is to keep that separation intact rather than pulling agent logic into OWUI's Pipelines framework (see section 6). + +## 5. Repository structure and branching + +At a high level, the core Agent Hub would be comprised of these three repositories, matching the natural seams in the system: + +```bash +studios-agent-hub/ # ← the main codebase, the OWUI fork +studio-agent-hub-agents/ # ← FastAPI + Bedrock agents (e.g. Sales Agent) +``` +***note**: repository names are placeholders at this stage and this outline don’t account for separate ‘infra’ style repositories to support deployment and CPV3 provisioning* + +The instance of LiteLLM and its proxy configuration is handled in a separate infra repository. + +### Example agent-hub structure + +```bash +agent-hub/ +├── branding/ # custom.css, logo and icon assets +├── deploy/ # (if cpv3 provisioning lives here) +│ ├── helm/values-cpv3.yaml +│ └── terraform/ +├── docs/adr/ +├── Dockerfile # FROM ghcr.io/open-webui/open-webui:vX.Y.Z + COPY branding/ +└── .github/workflows/ # build/push image, upstream diff checks, etc. +``` + +The original (vendored) OWUI source itself is not shown above because it’s not copied into the repository at all — it's pulled in via the fork relationship (git history) and consumed as a base image in the Dockerfile. + +Our Agent Hub specific additions sit alongside this as configuration, assets, and deployment tooling, never as edits mixed into OWUI's own source tree. This is what keeps upstream syncs (section 3) low-conflict. + +### Example branching model + +| Example branch | Used for | +| -------------- | -------- | +|`main` | Core production/release branch; what's actually deployed | +| upstream (remote) | `open-webui/open-webui`, tracked, never pushed to | +|`sync/vX.Y.Z` | short-lived branch per upstream release pulled in | +| `feature/AH-123`, `AH456-some-name` | standard Jira-ticket branches (existing team convention) | + +## 6. Model and agent governance via LiteLLM + +Agent Hub does not connect directly to approved models, or to our own agents (e.g. Sales Agent). All model traffic is routed through a LiteLLM proxy, which acts as the single governance point for ‘what's exposed and to whom’. + +### Decision and rationale + +- Foundation models (e.g. Bedrock, OpenAI) will be registered in LiteLLM as model deployments, so they present identically to Agent Hub as OpenAI-API-compatible ‘models’. +- Each FastAPI agent (e.g. Sales Agent) wraps its logic behind its own `/v1/chat/completions-compatible` endpoint which can then be connected to OWUI directly via the Pipelines framework. This enables us to support rich HTML or complex UI components called by and from agents or tools by dealing with OWUI's internal HTML renderer. +- This approach helps to maintain a restricted governance path: + - model allow-listing, + - per-team/per-key access control, + - budgets, + - rate limiting, + - and usage/cost tracking and metrics +- This also helps to keep the OWUI fork closer to stock, since very minimal custom pipe or connector code needs to live inside it. + +Sensitive data handling (e.g. the Sales Agent's access to Rights and financial data) happens entirely inside the agent's own API service, ahead of the call to Bedrock. OWUI and LiteLLM have no awareness of the underlying data sources — they only see a model that produces relevant answers. + +### Open item + +Model-level visibility (e.g. restricting the Sales Agent to specific teams) could be enforced at two independent layers: + +1. OWUI's group-based model visibility controls. +2. LiteLLM's own key/team-based access controls. + +The specific mapping of ITV's user groups to both layers is not yet finalised and will involve some sort of Okta grouping. + +## 7. Security and operational considerations + +- Functions, Tools, and Pipes execute arbitrary Python with the same file and network permissions as the OWUI process. **Community-sourced plugins should not be imported without code review**, particularly in a walled cpv3 environment. +- Outbound calls OWUI makes by default (community marketplace, update checks, telemetry) should be audited and disabled where not needed. +- Configuration that can persist to the database via the Admin UI (e.g. OAuth settings) should be pinned to environment variables (`ENABLE_OAUTH_PERSISTENT_CONFIG=false` and equivalents) to keep configuration reproducible and version-controlled rather than living as manual UI changes. + +## 8. Open questions + +1. Preferred cadence for pulling in upstream OWUI releases (e.g. monthly review vs. on-demand). +2. Model-level visibility (see ‘Open item’ in section 7 above). +3. Naming conventions, including repo name, branching, and so on. From 84af9e276f51be334c276ea7b5ff8a2416f450d5 Mon Sep 17 00:00:00 2001 From: Rob Kendal <38687284+bpk68@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:26:02 +0100 Subject: [PATCH 03/14] Add initial custom CSS file and branding folders --- branding/custom.css | 1 + 1 file changed, 1 insertion(+) create mode 100644 branding/custom.css diff --git a/branding/custom.css b/branding/custom.css new file mode 100644 index 00000000000..ca5207e6864 --- /dev/null +++ b/branding/custom.css @@ -0,0 +1 @@ +/* :root {} */ \ No newline at end of file From f0d46f880a8e99df020ce359c1335844da33af35 Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 10:18:08 +0100 Subject: [PATCH 04/14] Ignore updates to docker build and release workflow files --- .../workflows/studios-upstream-sync-pr.yml | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/studios-upstream-sync-pr.yml b/.github/workflows/studios-upstream-sync-pr.yml index d3589c97a2e..666fd1e3728 100644 --- a/.github/workflows/studios-upstream-sync-pr.yml +++ b/.github/workflows/studios-upstream-sync-pr.yml @@ -59,7 +59,26 @@ jobs: # 4. Create sync branch off main and merge tag git checkout -b "$BRANCH_NAME" main - git merge "$LATEST_TAG" -m "sync: merge upstream $LATEST_TAG" + # Merge without committing immediately + git merge "$LATEST_TAG" --no-commit --no-ff || true + + # Revert specified workflows back to your fork's HEAD state + FILES_TO_IGNORE=( + ".github/workflows/docker.yaml" + ".github/workflows/release.yml" + ".github/workflows/release-pypi.yml" + ) + + for file in "${FILES_TO_IGNORE[@]}"; do + if git cat-file -e "HEAD:$file" 2>/dev/null; then + git restore --staged --worktree --source=HEAD "$file" + else + git rm -f --ignore-unmatch "$file" + fi + done + + # Complete the merge commit and push + git commit -m "sync: merge upstream $LATEST_TAG (ignored specific workflows)" git push origin "$BRANCH_NAME" # 5. Open Pull Request via GitHub CLI From 57d315d167f133fe2db6e811dc0a2a6e355258c7 Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 10:34:34 +0100 Subject: [PATCH 05/14] Ignore all workflow updates in sync --- .../workflows/studios-upstream-sync-pr.yml | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/studios-upstream-sync-pr.yml b/.github/workflows/studios-upstream-sync-pr.yml index 666fd1e3728..62d8690e76c 100644 --- a/.github/workflows/studios-upstream-sync-pr.yml +++ b/.github/workflows/studios-upstream-sync-pr.yml @@ -62,23 +62,22 @@ jobs: # Merge without committing immediately git merge "$LATEST_TAG" --no-commit --no-ff || true - # Revert specified workflows back to your fork's HEAD state - FILES_TO_IGNORE=( - ".github/workflows/docker.yaml" - ".github/workflows/release.yml" - ".github/workflows/release-pypi.yml" - ) - - for file in "${FILES_TO_IGNORE[@]}"; do - if git cat-file -e "HEAD:$file" 2>/dev/null; then - git restore --staged --worktree --source=HEAD "$file" - else - git rm -f --ignore-unmatch "$file" - fi - done + # DO NOT SYNC github workflow files - the defualt GITHUB_TOKEN doesn't have permission to create PRs for workflows, so we ignore them + # Reset staged workflow changes to match fork's HEAD + git reset HEAD -- .github/workflows 2>/dev/null || true + + # Restore workflow files if they exist in fork, or remove if they don't + if git cat-file -e "HEAD:.github/workflows" 2>/dev/null; then + git checkout HEAD -- .github/workflows + else + rm -rf .github/workflows + fi + + # Clean any untracked workflow files introduced by upstream + git clean -fd .github/workflows 2>/dev/null || true # Complete the merge commit and push - git commit -m "sync: merge upstream $LATEST_TAG (ignored specific workflows)" + git commit -m "sync: merge upstream $LATEST_TAG (preserved fork workflows)" git push origin "$BRANCH_NAME" # 5. Open Pull Request via GitHub CLI From 5799c4a435ad5ba381c7888b4255f7c23b45f04c Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 10:44:19 +0100 Subject: [PATCH 06/14] Explicitly add repo to create pr against --- .github/workflows/studios-upstream-sync-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/studios-upstream-sync-pr.yml b/.github/workflows/studios-upstream-sync-pr.yml index 62d8690e76c..dbdbbe1d826 100644 --- a/.github/workflows/studios-upstream-sync-pr.yml +++ b/.github/workflows/studios-upstream-sync-pr.yml @@ -82,6 +82,7 @@ jobs: # 5. Open Pull Request via GitHub CLI gh pr create \ + --repo "$GITHUB_REPOSITORY" \ --title "sync: upstream release $LATEST_TAG" \ --body "Automated PR bringing upstream release \`$LATEST_TAG\` into \`main\`." \ --head "$BRANCH_NAME" \ From 021262f3bf49f824a11466217ef01b4d49e41bd3 Mon Sep 17 00:00:00 2001 From: Shahi Islam Date: Mon, 27 Jul 2026 11:54:04 +0100 Subject: [PATCH 07/14] Adding initial workflows --- .github/workflows/studios-CICD-pipeline.yaml | 45 ++++++++ .github/workflows/studios-build.yml | 114 +++++++++++++++++++ .github/workflows/studios-deploy.yml | 110 ++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 .github/workflows/studios-CICD-pipeline.yaml create mode 100644 .github/workflows/studios-build.yml create mode 100644 .github/workflows/studios-deploy.yml diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml new file mode 100644 index 00000000000..f322340273e --- /dev/null +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -0,0 +1,45 @@ +name: Studios Agent Hub Build and Deploy +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: studios-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + frontend: + uses: ./.github/workflows/frontend.yaml + backend: + uses: ./.github/workflows/backend.yaml + + build: + needs: [frontend, backend] + uses: ./.github/workflows/studios-build.yml + with: + productEcrRegistry: 590183674594.dkr.ecr.eu-west-1.amazonaws.com + awsRole: arn:aws:iam::590183674594:role/dev-ai-agent-hub-service-gha + serviceName: studios-agent-hub + pactsEnabled: false + integrationTests: false + dockerIntegrationTestStubs: false + publish: ${{ github.ref == 'refs/heads/main' }} + runsOn: 'dev-studios-agent-hub' + stg_deploy: + needs: build + uses: ./.github/workflows/studios-deploy.yml + with: + imageTag: ${{ needs.build.outputs.image_tag || 'latest' }} + ecosystem: dev + gitRef: ${{ github.ref }} + + prd_deploy: + needs: [build, stg_deploy] + uses: ./.github/workflows/studios-deploy.yml + with: + imageTag: ${{ needs.build.outputs.image_tag || 'latest' }} + ecosystem: prd + prdPromoteImage: true + gitRef: ${{ github.ref }} \ No newline at end of file diff --git a/.github/workflows/studios-build.yml b/.github/workflows/studios-build.yml new file mode 100644 index 00000000000..86a7029d265 --- /dev/null +++ b/.github/workflows/studios-build.yml @@ -0,0 +1,114 @@ +name: Studios Agent Hub Build +on: + workflow_call: + inputs: + productEcrRegistry: + description: 'ECR Registry' + required: true + type: string + awsRole: + description: 'AWS IAM Role' + required: false + type: string + serviceName: + description: 'Name of service' + required: true + type: string + publish: + description: 'Publish to ECR' + required: false + type: boolean + default: false + runsOn: + description: 'Defaults to ubuntu-latest, for self hosted runners use cd-runner-$serviceName-$tShirtSize' + required: false + type: string + default: 'ubuntu-latest' + outputs: + image_tag: + description: 'Tag of the built image' + value: ${{ jobs.build.outputs.image_tag }} + + secrets: + ARTIFACTORY_USERNAME: + description: 'Username to access JFrog.io published libraries in the build' + required: true + ARTIFACTORY_PASSWORD: + description: 'Password to access JFrog.io published libraries in the build' + required: true + AWS_ACCESS_KEY_ID: + description: 'AWS Access Key' + required: false + AWS_SECRET_ACCESS_KEY: + description: 'AWS Secret Key' + required: false + +env: + DOCKER_API_VERSION: "1.43" +jobs: + build: + timeout-minutes: 60 + permissions: + id-token: write + contents: write + runs-on: ${{ inputs.runsOn }} + outputs: + image_tag: ${{ steps.set-tag.outputs.image_tag }} + steps: + - uses: actions/checkout@v2 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: eu-west-1 + role-to-assume: ${{ inputs.awsRole }} + role-duration-seconds: 1200 + + - name: Log in to the root ECR + uses: docker/login-action@v2 + with: + registry: 655028521085.dkr.ecr.eu-west-1.amazonaws.com + + - name: Extract metadata (tags, labels) for service image + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ inputs.productEcrRegistry }}/${{ inputs.serviceName }} + tags: | + type=ref,event=branch,prefix=gha-,suffix=-{{sha}}-${{ github.run_number }} + type=ref,event=pr,prefix=gha-pr-,suffix=-{{sha}}-${{ github.run_number }} + + - name: Log in to the ECR + if: ${{ inputs.publish }} + uses: docker/login-action@v2 + with: + registry: ${{ inputs.productEcrRegistry }} + + - name: Set up docker context + run: docker context create builders + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + endpoint: builders + + - name: Build and push service image + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ inputs.publish }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + ARTIFACTORY_USERNAME=${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD=${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Tag built version + id: set-tag + if: ${{ inputs.publish }} + env: + TAG_VALUE: ${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + run: | + git tag $TAG_VALUE $GITHUB_SHA + git push origin $TAG_VALUE + echo "image_tag=$TAG_VALUE" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/workflows/studios-deploy.yml b/.github/workflows/studios-deploy.yml new file mode 100644 index 00000000000..f18861ca05f --- /dev/null +++ b/.github/workflows/studios-deploy.yml @@ -0,0 +1,110 @@ +name: Agent Hub Deploy + +on: + workflow_call: + inputs: + imageTag: + description: 'Docker image tag to deploy' + required: true + type: string + ecosystem: + description: 'Target APLO ecosystem (dev for stg, prd for production)' + required: true + type: string + prdPromoteImage: + description: 'Enable production image promotion' + required: false + type: boolean + default: false + gitRef: + description: 'Git reference override for APLO' + required: false + type: string + default: 'refs/heads/main' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Log in to the root ECR + uses: docker/login-action@v2 + with: + registry: 655028521085.dkr.ecr.eu-west-1.amazonaws.com + + - name: Log in to the ECR + if: ${{ inputs.publish }} + uses: docker/login-action@v2 + with: + registry: ${{ inputs.productEcrRegistry }} + + - name: Check prd testing image exists + if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && !inputs.BUILD_IN_PRD + run: | + TEST_TAG="${{ env.IMAGE_TO_DEPLOY }}-testing-image" + if ! PRD_TEST_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then + echo "Unable to describe PRD ECR repository for $TEST_TAG" >&2 + exit 1 + fi + PRD_TEST_TAGS=$(echo "$PRD_TEST_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) + + if echo "$PRD_TEST_TAGS" | grep -Fx "$TEST_TAG" >/dev/null; then + echo "TESTING_IMAGE_EXISTS=true" >> "$GITHUB_ENV" + echo "Detected testing image tag $TEST_TAG in PRD ECR" + else + echo "TESTING_IMAGE_EXISTS=" >> "$GITHUB_ENV" + echo "Testing image tag $TEST_TAG not found in PRD ECR; will attempt to copy from DEV if promotion is enabled" + fi + + - name: Copy testing image to prd + if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD + run: | + if [[ '${{ env.TESTING_IMAGE_EXISTS }}' == '' ]]; then + if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image; then + docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image + docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image + else + if [[ '${{ env.IS_ROLLBACK }}' == 'true' ]]; then + echo "⚠️ Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image not found in DEV ECR during rollback; deployment will continue but canary tests may be skipped or fail" + else + echo "Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image missing in DEV ECR; rerun build or ensure testing images are available" >&2 + exit 1 + fi + fi + else + echo "Testing image with tag ${{ env.IMAGE_TO_DEPLOY }}-testing-image already exists in the repository ${{ inputs.SERVICE }}" + exit 0 + fi + + - name: Check prd image exists + if: inputs.ECOSYSTEM == 'prd' && !inputs.BUILD_IN_PRD + run: | + IMAGE_TAG="${{ env.IMAGE_TO_DEPLOY }}" + if ! PRD_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then + echo "Unable to describe PRD ECR repository for $IMAGE_TAG" >&2 + exit 1 + fi + PRD_TAGS=$(echo "$PRD_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) + + if echo "$PRD_TAGS" | grep -Fx "$IMAGE_TAG" >/dev/null; then + echo "IMAGE_EXISTS=true" >> "$GITHUB_ENV" + echo "Detected image tag $IMAGE_TAG in PRD ECR" + else + echo "IMAGE_EXISTS=" >> "$GITHUB_ENV" + echo "Image tag $IMAGE_TAG not found in PRD ECR; attempting copy from DEV" + fi + + - name: Copy image to prd + if: inputs.ECOSYSTEM == 'prd' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD + run: | + if [[ '${{ env.IMAGE_EXISTS }}' == '' ]]; then + if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}; then + docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} + docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} + else + echo "Failed to pull image ${{ env.IMAGE_TO_DEPLOY }} from DEV ECR; confirm the tag exists or rebuild the image before retrying." + exit 1 + fi + else + echo "Image with tag ${{ env.IMAGE_TO_DEPLOY }} already exists in the repository ${{ inputs.SERVICE }}" + exit 0 + fi From 6f7fb2a54d9f75056283f05b414ebabcd4658fbb Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Mon, 27 Jul 2026 12:34:42 +0100 Subject: [PATCH 08/14] Fix formatting of docs --- branding/custom.css | 2 +- docs/adr/repo-structure.md | 55 +++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/branding/custom.css b/branding/custom.css index ca5207e6864..6dc13fe0ceb 100644 --- a/branding/custom.css +++ b/branding/custom.css @@ -1 +1 @@ -/* :root {} */ \ No newline at end of file +/* :root {} */ diff --git a/docs/adr/repo-structure.md b/docs/adr/repo-structure.md index f15c79d8599..f55ef2cffd5 100644 --- a/docs/adr/repo-structure.md +++ b/docs/adr/repo-structure.md @@ -1,4 +1,4 @@ -# ITV AI Agent Hub - OpenWebUI (OWUI) repo, structure, and updating strategy +# ITV AI Agent Hub - OpenWebUI (OWUI) repo, structure, and updating strategy ## Architecture decisions and recommendations @@ -8,9 +8,9 @@ This document captures the key decisions for ITV Studios Agent Hub, specifically Decisions here reflect the engineering team’s current thinking and should be revisited as the platform matures and OWUI itself evolves. -## 2. Licensing +## 2. Licensing -OWUI moved from a permissive BSD-3-Clause licence to a custom 'Open WebUI Licence' at version 0.6.6 around April 2025. This licence retains permissive use and modification rights, but adds a branding protection clause: +OWUI moved from a permissive BSD-3-Clause licence to a custom 'Open WebUI Licence' at version 0.6.6 around April 2025. This licence retains permissive use and modification rights, but adds a branding protection clause: > “deployments with more than 50 aggregate end users in any rolling 30-day period may not remove or alter the 'OWUI' name, logo, or UI marks unless they hold an Enterprise licence or specific written permission.” @@ -26,11 +26,11 @@ This decision removes licensing as a constraint on the technical approach to how ## 3. Repository forking and upstream sync strategy -The core OWUI product is a living codebase; an ongoing concern in active development and maintenance. This means we can benefit from OWUI’s engineering team producing new features, pushing bug fixes, and closing security vulnerabilities. Whilst this is an absolute win for the business, it creates a challenge for the Agent Hub Delivery Team in bringing these changes into our product from the upstream codebase. +The core OWUI product is a living codebase; an ongoing concern in active development and maintenance. This means we can benefit from OWUI’s engineering team producing new features, pushing bug fixes, and closing security vulnerabilities. Whilst this is an absolute win for the business, it creates a challenge for the Agent Hub Delivery Team in bringing these changes into our product from the upstream codebase. There are two approaches to take: -1. We pull a fork of the original OWUI project and track it as an upstream remote and merge in new tagged releases as they become available. +1. We pull a fork of the original OWUI project and track it as an upstream remote and merge in new tagged releases as they become available. 2. We create a brand new repository based on the latest version of OWUI and manually merge in updates and releases as we diverge away from the original codebase. ### Decision @@ -53,16 +53,16 @@ A path exists to contribute fixes back upstream to the main OWUI project where u ## 4. Customisation approach -OWUI is layered in a way that supports the Agent Hub Delivery Team’s goal of staying easy to update and keep in sync with upstream releases. +OWUI is layered in a way that supports the Agent Hub Delivery Team’s goal of staying easy to update and keep in sync with upstream releases. The proposed approach below favours the lowest-friction customisation layer available for each need: -| Layer | Use for | -|------ | --------| -|**CSS theming**
(custom.css, injected via our own Docker image layer) | Branding, colours, fonts, ITV visual identity — no core source changes required | -| **Functions, Tools, Filters**
(in-process Python plugins, managed via Admin Panel) | Lightweight behavioural changes: message filters, custom UI buttons, new model source wiring | -| **Pipelines**
(standalone microservice, OpenAI-API-compatible) | Heavier processing that shouldn't run inside the OWUI process itself | -| **Direct source modification** | Avoided wherever possible — reserved for cases with no other option, since this is where upstream merge conflicts arise | +| Layer | Use for | +| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **CSS theming**
(custom.css, injected via our own Docker image layer) | Branding, colours, fonts, ITV visual identity — no core source changes required | +| **Functions, Tools, Filters**
(in-process Python plugins, managed via Admin Panel) | Lightweight behavioural changes: message filters, custom UI buttons, new model source wiring | +| **Pipelines**
(standalone microservice, OpenAI-API-compatible) | Heavier processing that shouldn't run inside the OWUI process itself | +| **Direct source modification** | Avoided wherever possible — reserved for cases with no other option, since this is where upstream merge conflicts arise | It’s recommended that we minimise any direct customisation of OWUI’s existing codebase to reduce treacherous version updates. Modifications should be created as separate modules or Svelte components and brought into their relevant areas in as discreet a manner as possible. For example, adding custom buttons to the chat window or specialist HTML responses. @@ -76,7 +76,8 @@ At a high level, the core Agent Hub would be comprised of these three repositori studios-agent-hub/ # ← the main codebase, the OWUI fork studio-agent-hub-agents/ # ← FastAPI + Bedrock agents (e.g. Sales Agent) ``` -***note**: repository names are placeholders at this stage and this outline don’t account for separate ‘infra’ style repositories to support deployment and CPV3 provisioning* + +**\*note**: repository names are placeholders at this stage and this outline don’t account for separate ‘infra’ style repositories to support deployment and CPV3 provisioning\* The instance of LiteLLM and its proxy configuration is handled in a separate infra repository. @@ -87,8 +88,8 @@ agent-hub/ ├── branding/ # custom.css, logo and icon assets ├── deploy/ # (if cpv3 provisioning lives here) │ ├── helm/values-cpv3.yaml -│ └── terraform/ -├── docs/adr/ +│ └── terraform/ +├── docs/adr/ ├── Dockerfile # FROM ghcr.io/open-webui/open-webui:vX.Y.Z + COPY branding/ └── .github/workflows/ # build/push image, upstream diff checks, etc. ``` @@ -99,11 +100,11 @@ Our Agent Hub specific additions sit alongside this as configuration, assets, an ### Example branching model -| Example branch | Used for | -| -------------- | -------- | -|`main` | Core production/release branch; what's actually deployed | -| upstream (remote) | `open-webui/open-webui`, tracked, never pushed to | -|`sync/vX.Y.Z` | short-lived branch per upstream release pulled in | +| Example branch | Used for | +| ----------------------------------- | -------------------------------------------------------- | +| `main` | Core production/release branch; what's actually deployed | +| upstream (remote) | `open-webui/open-webui`, tracked, never pushed to | +| `sync/vX.Y.Z` | short-lived branch per upstream release pulled in | | `feature/AH-123`, `AH456-some-name` | standard Jira-ticket branches (existing team convention) | ## 6. Model and agent governance via LiteLLM @@ -114,22 +115,22 @@ Agent Hub does not connect directly to approved models, or to our own agents (e. - Foundation models (e.g. Bedrock, OpenAI) will be registered in LiteLLM as model deployments, so they present identically to Agent Hub as OpenAI-API-compatible ‘models’. - Each FastAPI agent (e.g. Sales Agent) wraps its logic behind its own `/v1/chat/completions-compatible` endpoint which can then be connected to OWUI directly via the Pipelines framework. This enables us to support rich HTML or complex UI components called by and from agents or tools by dealing with OWUI's internal HTML renderer. -- This approach helps to maintain a restricted governance path: - - model allow-listing, +- This approach helps to maintain a restricted governance path: + - model allow-listing, - per-team/per-key access control, - - budgets, - - rate limiting, + - budgets, + - rate limiting, - and usage/cost tracking and metrics -- This also helps to keep the OWUI fork closer to stock, since very minimal custom pipe or connector code needs to live inside it. +- This also helps to keep the OWUI fork closer to stock, since very minimal custom pipe or connector code needs to live inside it. Sensitive data handling (e.g. the Sales Agent's access to Rights and financial data) happens entirely inside the agent's own API service, ahead of the call to Bedrock. OWUI and LiteLLM have no awareness of the underlying data sources — they only see a model that produces relevant answers. ### Open item -Model-level visibility (e.g. restricting the Sales Agent to specific teams) could be enforced at two independent layers: +Model-level visibility (e.g. restricting the Sales Agent to specific teams) could be enforced at two independent layers: 1. OWUI's group-based model visibility controls. -2. LiteLLM's own key/team-based access controls. +2. LiteLLM's own key/team-based access controls. The specific mapping of ITV's user groups to both layers is not yet finalised and will involve some sort of Okta grouping. From 65763be2adee1836552206b4e0248ea08ce34d98 Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 11:00:48 +0100 Subject: [PATCH 09/14] temp run from branch --- .github/workflows/studios-CICD-pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml index f322340273e..42fa77ee09a 100644 --- a/.github/workflows/studios-CICD-pipeline.yaml +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - AH-125_workflows workflow_dispatch: concurrency: From bbd7116ed3d9cfc3b90923851098564a36df3131 Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 11:03:43 +0100 Subject: [PATCH 10/14] Use custom FE & BE workflow files --- .github/workflows/studios-CICD-pipeline.yaml | 4 +- .github/workflows/studios-backend.yaml | 38 +++++++++++++ .github/workflows/studios-frontend.yaml | 58 ++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/studios-backend.yaml create mode 100644 .github/workflows/studios-frontend.yaml diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml index 42fa77ee09a..56d1b992b30 100644 --- a/.github/workflows/studios-CICD-pipeline.yaml +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -12,9 +12,9 @@ concurrency: jobs: frontend: - uses: ./.github/workflows/frontend.yaml + uses: ./.github/workflows/studios-frontend.yaml backend: - uses: ./.github/workflows/backend.yaml + uses: ./.github/workflows/studios-backend.yaml build: needs: [frontend, backend] diff --git a/.github/workflows/studios-backend.yaml b/.github/workflows/studios-backend.yaml new file mode 100644 index 00000000000..a8f60d9f5d8 --- /dev/null +++ b/.github/workflows/studios-backend.yaml @@ -0,0 +1,38 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Backend CI — Python formatting checks via Ruff +# Runs on pushes and PRs to main/dev when backend files change +# ───────────────────────────────────────────────────────────────────────────── +name: Python CI + +on: + workflow_call: + +concurrency: + group: backend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Ruff format check across supported Python versions ─────────────────── + format-check: + name: Ruff Format (${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install formatter + run: pip install "ruff>=0.15.5" + + - name: Verify formatting + run: ruff format --check . --exclude .venv --exclude venv + + - name: Detect logic errors + run: ruff check --select=F --ignore=F401,F403,F405,F541,F811,F841 --output-format=github . diff --git a/.github/workflows/studios-frontend.yaml b/.github/workflows/studios-frontend.yaml new file mode 100644 index 00000000000..10834181716 --- /dev/null +++ b/.github/workflows/studios-frontend.yaml @@ -0,0 +1,58 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Frontend CI — Lint, format check, build, and unit tests +# Runs on pushes and PRs to main/dev, skipping backend-only changes +# ───────────────────────────────────────────────────────────────────────────── +name: Frontend Build + +on: + workflow_call: + +concurrency: + group: frontend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Format, i18n, and production build ──────────────────────────────────── + format-and-build: + name: Format & Build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + run: npm install --force + + - name: Verify code formatting + run: npm run format + + - name: Verify i18n strings + run: npm run i18n:parse + + - name: Ensure working tree is clean + run: git diff --exit-code + + - name: Production build + run: npm run build + + # ── Vitest unit tests ──────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies (frozen lockfile) + run: npm ci --force + + - name: Execute test suite + run: npm run test:frontend From ec207dfa40bbda7da8be027ff453fd1a7da673bc Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 11:06:58 +0100 Subject: [PATCH 11/14] Update workflow --- .github/workflows/studios-CICD-pipeline.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml index 56d1b992b30..25818640303 100644 --- a/.github/workflows/studios-CICD-pipeline.yaml +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -23,9 +23,6 @@ jobs: productEcrRegistry: 590183674594.dkr.ecr.eu-west-1.amazonaws.com awsRole: arn:aws:iam::590183674594:role/dev-ai-agent-hub-service-gha serviceName: studios-agent-hub - pactsEnabled: false - integrationTests: false - dockerIntegrationTestStubs: false publish: ${{ github.ref == 'refs/heads/main' }} runsOn: 'dev-studios-agent-hub' stg_deploy: From b2e9a38d65c5eac309725d637095abe4abf0f213 Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 11:15:16 +0100 Subject: [PATCH 12/14] remove unused Artifactory secret --- .github/workflows/studios-CICD-pipeline.yaml | 2 +- .github/workflows/studios-build.yml | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml index 25818640303..23c7ac5fb47 100644 --- a/.github/workflows/studios-CICD-pipeline.yaml +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -5,7 +5,7 @@ on: - main - AH-125_workflows workflow_dispatch: - + concurrency: group: studios-ci-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/studios-build.yml b/.github/workflows/studios-build.yml index 86a7029d265..fbfae439f6b 100644 --- a/.github/workflows/studios-build.yml +++ b/.github/workflows/studios-build.yml @@ -30,12 +30,6 @@ on: value: ${{ jobs.build.outputs.image_tag }} secrets: - ARTIFACTORY_USERNAME: - description: 'Username to access JFrog.io published libraries in the build' - required: true - ARTIFACTORY_PASSWORD: - description: 'Password to access JFrog.io published libraries in the build' - required: true AWS_ACCESS_KEY_ID: description: 'AWS Access Key' required: false @@ -99,9 +93,9 @@ jobs: push: ${{ inputs.publish }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - build-args: | - ARTIFACTORY_USERNAME=${{ secrets.ARTIFACTORY_USERNAME }} - ARTIFACTORY_PASSWORD=${{ secrets.ARTIFACTORY_PASSWORD }} + # build-args: | + # ARTIFACTORY_USERNAME=${{ secrets.ARTIFACTORY_USERNAME }} + # ARTIFACTORY_PASSWORD=${{ secrets.ARTIFACTORY_PASSWORD }} - name: Tag built version id: set-tag From f2dea0014070459efa39ba215515549905efc19b Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 11:44:17 +0100 Subject: [PATCH 13/14] Try hacky build using aplo --- .github/workflows/studios-CICD-pipeline.yaml | 4 +- .github/workflows/studios-deploy.yml | 100 +++-------------- .github/workflows/studios-deploy_old.yml | 110 +++++++++++++++++++ .github/workflows/studios-frontend.yaml | 5 +- 4 files changed, 130 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/studios-deploy_old.yml diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml index 23c7ac5fb47..6795998a8d5 100644 --- a/.github/workflows/studios-CICD-pipeline.yaml +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -32,6 +32,7 @@ jobs: imageTag: ${{ needs.build.outputs.image_tag || 'latest' }} ecosystem: dev gitRef: ${{ github.ref }} + secrets: inherit prd_deploy: needs: [build, stg_deploy] @@ -40,4 +41,5 @@ jobs: imageTag: ${{ needs.build.outputs.image_tag || 'latest' }} ecosystem: prd prdPromoteImage: true - gitRef: ${{ github.ref }} \ No newline at end of file + gitRef: ${{ github.ref }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/studios-deploy.yml b/.github/workflows/studios-deploy.yml index f18861ca05f..a8de1d161e0 100644 --- a/.github/workflows/studios-deploy.yml +++ b/.github/workflows/studios-deploy.yml @@ -21,90 +21,22 @@ on: required: false type: string default: 'refs/heads/main' + secrets: + INTERNAL_REPO_RO_PAT: + required: true jobs: deploy: - runs-on: ubuntu-latest - steps: - - name: Log in to the root ECR - uses: docker/login-action@v2 - with: - registry: 655028521085.dkr.ecr.eu-west-1.amazonaws.com - - - name: Log in to the ECR - if: ${{ inputs.publish }} - uses: docker/login-action@v2 - with: - registry: ${{ inputs.productEcrRegistry }} - - - name: Check prd testing image exists - if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && !inputs.BUILD_IN_PRD - run: | - TEST_TAG="${{ env.IMAGE_TO_DEPLOY }}-testing-image" - if ! PRD_TEST_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then - echo "Unable to describe PRD ECR repository for $TEST_TAG" >&2 - exit 1 - fi - PRD_TEST_TAGS=$(echo "$PRD_TEST_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) - - if echo "$PRD_TEST_TAGS" | grep -Fx "$TEST_TAG" >/dev/null; then - echo "TESTING_IMAGE_EXISTS=true" >> "$GITHUB_ENV" - echo "Detected testing image tag $TEST_TAG in PRD ECR" - else - echo "TESTING_IMAGE_EXISTS=" >> "$GITHUB_ENV" - echo "Testing image tag $TEST_TAG not found in PRD ECR; will attempt to copy from DEV if promotion is enabled" - fi - - - name: Copy testing image to prd - if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD - run: | - if [[ '${{ env.TESTING_IMAGE_EXISTS }}' == '' ]]; then - if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image; then - docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image - docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image - else - if [[ '${{ env.IS_ROLLBACK }}' == 'true' ]]; then - echo "⚠️ Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image not found in DEV ECR during rollback; deployment will continue but canary tests may be skipped or fail" - else - echo "Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image missing in DEV ECR; rerun build or ensure testing images are available" >&2 - exit 1 - fi - fi - else - echo "Testing image with tag ${{ env.IMAGE_TO_DEPLOY }}-testing-image already exists in the repository ${{ inputs.SERVICE }}" - exit 0 - fi - - - name: Check prd image exists - if: inputs.ECOSYSTEM == 'prd' && !inputs.BUILD_IN_PRD - run: | - IMAGE_TAG="${{ env.IMAGE_TO_DEPLOY }}" - if ! PRD_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then - echo "Unable to describe PRD ECR repository for $IMAGE_TAG" >&2 - exit 1 - fi - PRD_TAGS=$(echo "$PRD_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) - - if echo "$PRD_TAGS" | grep -Fx "$IMAGE_TAG" >/dev/null; then - echo "IMAGE_EXISTS=true" >> "$GITHUB_ENV" - echo "Detected image tag $IMAGE_TAG in PRD ECR" - else - echo "IMAGE_EXISTS=" >> "$GITHUB_ENV" - echo "Image tag $IMAGE_TAG not found in PRD ECR; attempting copy from DEV" - fi - - - name: Copy image to prd - if: inputs.ECOSYSTEM == 'prd' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD - run: | - if [[ '${{ env.IMAGE_EXISTS }}' == '' ]]; then - if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}; then - docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} - docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} - else - echo "Failed to pull image ${{ env.IMAGE_TO_DEPLOY }} from DEV ECR; confirm the tag exists or rebuild the image before retrying." - exit 1 - fi - else - echo "Image with tag ${{ env.IMAGE_TO_DEPLOY }} already exists in the repository ${{ inputs.SERVICE }}" - exit 0 - fi + uses: ITV/cp-gha-workflows/.github/workflows/aplo.yml@WF_aplo_v2.5.0 + with: + ECOSYSTEM: ${{ inputs.ecosystem }} + PRODUCT: ca + SERVICE: studios-airtable-catalogue + GIT_BRANCH: main + GIT_REF_OVERRIDE: ${{ inputs.gitRef }} + IMAGE_TAG: ${{ inputs.imageTag }} + BUILD_ENABLED: false + PRD_PROMOTE_IMAGE: ${{ inputs.prdPromoteImage }} + DEPLOY_ENABLED: false + secrets: + INTERNAL_REPO_RO_PAT: ${{ secrets.INTERNAL_REPO_RO_PAT }} \ No newline at end of file diff --git a/.github/workflows/studios-deploy_old.yml b/.github/workflows/studios-deploy_old.yml new file mode 100644 index 00000000000..780305f8034 --- /dev/null +++ b/.github/workflows/studios-deploy_old.yml @@ -0,0 +1,110 @@ +name: Agent Hub Deploy Old + +on: + workflow_call: + inputs: + imageTag: + description: 'Docker image tag to deploy' + required: true + type: string + ecosystem: + description: 'Target APLO ecosystem (dev for stg, prd for production)' + required: true + type: string + prdPromoteImage: + description: 'Enable production image promotion' + required: false + type: boolean + default: false + gitRef: + description: 'Git reference override for APLO' + required: false + type: string + default: 'refs/heads/main' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Log in to the root ECR + uses: docker/login-action@v2 + with: + registry: 655028521085.dkr.ecr.eu-west-1.amazonaws.com + + - name: Log in to the ECR + if: ${{ inputs.publish }} + uses: docker/login-action@v2 + with: + registry: ${{ inputs.productEcrRegistry }} + + - name: Check prd testing image exists + if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && !inputs.BUILD_IN_PRD + run: | + TEST_TAG="${{ env.IMAGE_TO_DEPLOY }}-testing-image" + if ! PRD_TEST_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then + echo "Unable to describe PRD ECR repository for $TEST_TAG" >&2 + exit 1 + fi + PRD_TEST_TAGS=$(echo "$PRD_TEST_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) + + if echo "$PRD_TEST_TAGS" | grep -Fx "$TEST_TAG" >/dev/null; then + echo "TESTING_IMAGE_EXISTS=true" >> "$GITHUB_ENV" + echo "Detected testing image tag $TEST_TAG in PRD ECR" + else + echo "TESTING_IMAGE_EXISTS=" >> "$GITHUB_ENV" + echo "Testing image tag $TEST_TAG not found in PRD ECR; will attempt to copy from DEV if promotion is enabled" + fi + + - name: Copy testing image to prd + if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD + run: | + if [[ '${{ env.TESTING_IMAGE_EXISTS }}' == '' ]]; then + if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image; then + docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image + docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image + else + if [[ '${{ env.IS_ROLLBACK }}' == 'true' ]]; then + echo "⚠️ Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image not found in DEV ECR during rollback; deployment will continue but canary tests may be skipped or fail" + else + echo "Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image missing in DEV ECR; rerun build or ensure testing images are available" >&2 + exit 1 + fi + fi + else + echo "Testing image with tag ${{ env.IMAGE_TO_DEPLOY }}-testing-image already exists in the repository ${{ inputs.SERVICE }}" + exit 0 + fi + + - name: Check prd image exists + if: inputs.ECOSYSTEM == 'prd' && !inputs.BUILD_IN_PRD + run: | + IMAGE_TAG="${{ env.IMAGE_TO_DEPLOY }}" + if ! PRD_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then + echo "Unable to describe PRD ECR repository for $IMAGE_TAG" >&2 + exit 1 + fi + PRD_TAGS=$(echo "$PRD_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) + + if echo "$PRD_TAGS" | grep -Fx "$IMAGE_TAG" >/dev/null; then + echo "IMAGE_EXISTS=true" >> "$GITHUB_ENV" + echo "Detected image tag $IMAGE_TAG in PRD ECR" + else + echo "IMAGE_EXISTS=" >> "$GITHUB_ENV" + echo "Image tag $IMAGE_TAG not found in PRD ECR; attempting copy from DEV" + fi + + - name: Copy image to prd + if: inputs.ECOSYSTEM == 'prd' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD + run: | + if [[ '${{ env.IMAGE_EXISTS }}' == '' ]]; then + if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}; then + docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} + docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} + else + echo "Failed to pull image ${{ env.IMAGE_TO_DEPLOY }} from DEV ECR; confirm the tag exists or rebuild the image before retrying." + exit 1 + fi + else + echo "Image with tag ${{ env.IMAGE_TO_DEPLOY }} already exists in the repository ${{ inputs.SERVICE }}" + exit 0 + fi diff --git a/.github/workflows/studios-frontend.yaml b/.github/workflows/studios-frontend.yaml index 10834181716..a3f08074070 100644 --- a/.github/workflows/studios-frontend.yaml +++ b/.github/workflows/studios-frontend.yaml @@ -14,7 +14,7 @@ concurrency: jobs: # ── Format, i18n, and production build ──────────────────────────────────── format-and-build: - name: Format & Build + name: Format runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -36,9 +36,6 @@ jobs: - name: Ensure working tree is clean run: git diff --exit-code - - name: Production build - run: npm run build - # ── Vitest unit tests ──────────────────────────────────────────────────── unit-tests: name: Unit Tests From 95c18c0dfd306d1d19f0d1552c95e8e8e5dd7aaf Mon Sep 17 00:00:00 2001 From: Hugh Lunt Date: Wed, 29 Jul 2026 11:54:14 +0100 Subject: [PATCH 14/14] Use commit hash for aplo ref --- .github/workflows/studios-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/studios-deploy.yml b/.github/workflows/studios-deploy.yml index a8de1d161e0..852beac70ce 100644 --- a/.github/workflows/studios-deploy.yml +++ b/.github/workflows/studios-deploy.yml @@ -27,7 +27,7 @@ on: jobs: deploy: - uses: ITV/cp-gha-workflows/.github/workflows/aplo.yml@WF_aplo_v2.5.0 + uses: ITV/cp-gha-workflows/.github/workflows/aplo.yml@378a119ba22f72f30b95ed76485504bbca6a189c with: ECOSYSTEM: ${{ inputs.ecosystem }} PRODUCT: ca