Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
7 changes: 3 additions & 4 deletions .agents/skills/land-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,9 @@ pnpm run db:draft-changelog # entities changed (needs Docker); then prune the
pnpm run db:generate-erd-docs # after any changelog change
```

`generate:api:application-server:specs` **fails when a port it needs is busy** — HTTP, management, or
the JMX port it defaults to. It restores the previous spec rather than committing an empty one, so
the cost is a wasted Maven cycle. Pass free ports; the exact invocation is in `server/AGENTS.md`
§ OpenAPI generation ports.
`generate:api:application-server:specs` packages the reactor and boots the executable JAR on ports
it allocates itself, so nothing needs freeing; `server/AGENTS.md` § OpenAPI generation has the
`HEPHAESTUS_APPLICATION_JAR` shortcut for a JAR you already built.

## 5. Run the tests your diff can break

Expand Down
5 changes: 5 additions & 0 deletions .changeset/bright-supply-chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hephaestus": patch
---

Makes the agent and PostgreSQL images reproducible by installing the agent SDK from a committed lockfile and pinning every supported PostgreSQL base image by digest.
7 changes: 3 additions & 4 deletions .claude/skills/land-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,9 @@ pnpm run db:draft-changelog # entities changed (needs Docker); then prune the
pnpm run db:generate-erd-docs # after any changelog change
```

`generate:api:application-server:specs` **fails when a port it needs is busy** — HTTP, management, or
the JMX port it defaults to. It restores the previous spec rather than committing an empty one, so
the cost is a wasted Maven cycle. Pass free ports; the exact invocation is in `server/AGENTS.md`
§ OpenAPI generation ports.
`generate:api:application-server:specs` packages the reactor and boots the executable JAR on ports
it allocates itself, so nothing needs freeing; `server/AGENTS.md` § OpenAPI generation has the
`HEPHAESTUS_APPLICATION_JAR` shortcut for a JAR you already built.

## 5. Run the tests your diff can break

Expand Down
34 changes: 34 additions & 0 deletions .github/actions/restore-server-build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: "Restore the built server"
description: "Downloads the packaged reactor into server/ and installs its generated-clients JAR, so Maven goals run against the built classes instead of compiling again."
inputs:
artifact:
description: "Name of the reactor artifact uploaded by the package job"
required: true
outputs:
executable-jar:
description: "Absolute path of the executable application JAR"
value: ${{ steps.locate.outputs.executable-jar }}

runs:
using: "composite"
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.artifact }}
path: server

- name: Install the generated clients and locate the executable JAR
id: locate
shell: bash
working-directory: server
run: |
set -euo pipefail
test -d application/target/classes
test -d application/target/test-classes
mapfile -t jars < <(find application/target -maxdepth 1 -name 'hephaestus-application-*.jar')
mapfile -t clients < <(find generated-clients/target -maxdepth 1 -name 'hephaestus-generated-clients-*.jar')
(( ${#jars[@]} == 1 )) && (( ${#clients[@]} == 1 ))
# Single-module goals resolve the sibling from the local repository, not from the reactor.
./mvnw --batch-mode --quiet -Dmaven.build.cache.enabled=false install:install-file \
-Dfile="${clients[0]}" -DpomFile=generated-clients/pom.xml
echo "executable-jar=$PWD/${jars[0]}" >> "$GITHUB_OUTPUT"
14 changes: 7 additions & 7 deletions .github/actions/setup-caches/action.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: "Setup CI caches"
description: "Set up the JDK and cache Maven dependencies and generated clients."
description: "Set up the JDK and cache Maven dependencies; reactor jobs also restore the generated-client build cache."
inputs:
cache-type:
description: "Type of cache to setup"
description: "application-server-reactor for jobs that run the Maven reactor; application-server-tests for jobs that run goals against the packaged reactor"
required: true
os:
description: "Operating system for cache key"
Expand All @@ -18,13 +18,13 @@ runs:
CACHE_TYPE: ${{ inputs.cache-type }}
run: |
case "$CACHE_TYPE" in
application-server-quality|application-server-verification|application-server-integration|server-contracts|webapp-e2e) ;;
application-server-reactor|application-server-tests) ;;
*) echo "::error::Unknown cache type '$CACHE_TYPE'."; exit 1 ;;
esac

- name: Compute generated-client cache identity
id: generated-clients-identity
if: startsWith(inputs.cache-type, 'application-server-') || contains(fromJSON('["webapp-e2e", "server-contracts"]'), inputs.cache-type)
if: inputs.cache-type == 'application-server-reactor'
shell: bash
env:
DEPENDENCY_HASH: ${{ hashFiles('server/pom.xml', 'server/generated-clients/pom.xml', 'server/.mvn/**', 'server/generated-clients/src/**') }}
Expand All @@ -37,7 +37,6 @@ runs:

- name: Set up JDK 21
id: java
if: startsWith(inputs.cache-type, 'application-server-') || contains(fromJSON('["webapp-e2e", "server-contracts"]'), inputs.cache-type)
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: "temurin"
Expand All @@ -50,15 +49,16 @@ runs:
cache-read-only: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) || !contains(fromJSON('["push", "schedule", "workflow_dispatch"]'), github.event_name) }}
cache-jdk: false

# Merge groups branch from main and read its caches like a pull request does.
- name: Restore generated clients
if: github.event_name == 'pull_request' && (startsWith(inputs.cache-type, 'application-server-') || contains(fromJSON('["webapp-e2e", "server-contracts"]'), inputs.cache-type))
if: inputs.cache-type == 'application-server-reactor' && contains(fromJSON('["pull_request", "merge_group"]'), github.event_name)
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/build-cache
key: ${{ inputs.os }}-${{ steps.java.outputs.distribution }}-${{ steps.java.outputs.version }}-generated-clients-${{ steps.generated-clients-identity.outputs.hash }}

- name: Cache generated clients
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && contains(fromJSON('["push", "schedule", "workflow_dispatch"]'), github.event_name) && (startsWith(inputs.cache-type, 'application-server-') || contains(fromJSON('["webapp-e2e", "server-contracts"]'), inputs.cache-type))
if: inputs.cache-type == 'application-server-reactor' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && contains(fromJSON('["push", "schedule", "workflow_dispatch"]'), github.event_name)
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/build-cache
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Build

on:
workflow_call:
inputs:
should_skip:
description: "Whether to skip the workflow"
required: false
type: string
default: "false"
server_changed:
description: "Whether anything that runs the built server changed"
required: false
type: string
default: "true"
server_image_changed:
description: "Whether the application-server image inputs changed"
required: false
type: string
default: "true"
outputs:
server-artifact:
description: "Name of the artifact holding the packaged reactor"
value: ${{ jobs.server-package.outputs.artifact }}

permissions: {}

env:
SERVER_BUILD_ARTIFACT: server-build-${{ github.run_id }}

jobs:
# Compiles and packages the reactor exactly once; every server test tier, the browser E2E suite,
# the API and database contracts and the application image consume this artifact.
server-package:
name: "App Server: Package"
if: inputs.should_skip != 'true' && (inputs.server_changed == 'true' || inputs.server_image_changed == 'true')
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
artifact: ${{ env.SERVER_BUILD_ARTIFACT }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-caches
with:
cache-type: application-server-reactor
os: ${{ runner.os }}
- name: Package the reactor
working-directory: server
run: ./mvnw -pl application -am package -DskipTests --batch-mode
- name: Validate the packaged reactor
working-directory: server
run: |
set -euo pipefail
test -d application/target/classes
test -d application/target/test-classes
mapfile -t jars < <(find application/target -maxdepth 1 -name 'hephaestus-application-*.jar' ! -name '*-sources.jar' ! -name '*-javadoc.jar')
mapfile -t clients < <(find generated-clients/target -maxdepth 1 -name 'hephaestus-generated-clients-*.jar' ! -name '*-sources.jar' ! -name '*-javadoc.jar')
(( ${#jars[@]} == 1 )) && (( ${#clients[@]} == 1 ))
unzip -p "${jars[0]}" META-INF/MANIFEST.MF | grep -q '^Main-Class: org.springframework.boot.loader'
- name: Upload the packaged reactor
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.SERVER_BUILD_ARTIFACT }}
path: |
server/application/target/classes
server/application/target/test-classes
server/application/target/hephaestus-application-*.jar
server/generated-clients/target/hephaestus-generated-clients-*.jar
!server/**/*-sources.jar
!server/**/*-javadoc.jar
if-no-files-found: error
retention-days: 1
compression-level: 0
# A re-run of failed jobs keeps this artifact; a re-run of all jobs replaces it.
overwrite: true

application-server-image:
name: "App Server image"
needs: server-package
if: inputs.should_skip != 'true' && inputs.server_image_changed == 'true'
uses: ./.github/workflows/reusable-docker-build.yml
permissions:
contents: read
packages: write
id-token: write
attestations: write
with:
image-name: "hephaestus-build/application-server"
use-buildpacks: true
application-artifact: ${{ needs.server-package.outputs.artifact }}
project-descriptor: "server/application/project.toml"
# renovate: datasource=docker depName=paketobuildpacks/ubuntu-noble-run-tiny
run-image: "paketobuildpacks/ubuntu-noble-run-tiny@sha256:c32333227b6dfbc2a4a93b03ee6d3475cfe152468c7d396bf22f06099bb0ecc9"
registry: "ghcr.io"
single-arch: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
labels: |
org.opencontainers.image.title=Hephaestus Application Server
org.opencontainers.image.description=Spring Boot server for Hephaestus
org.opencontainers.image.vendor=AET TUM
org.opencontainers.image.licenses=MIT
hephaestus.component=application-server
39 changes: 2 additions & 37 deletions .github/workflows/ci-docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ on:
type: string
default: "true"
application_server_changed:
description: "Whether application-server files changed (or should build for other reasons)"
description: "Whether the Build workflow published an application-server image this run"
required: false
type: string
default: "true"
Expand All @@ -36,11 +36,6 @@ on:
type: string
default: "true"

outputs:
application-server-published:
description: "Whether an application-server image exists at this commit"
value: ${{ jobs.application-server-build.outputs.manifest-digest != '' || jobs.tag-unchanged-images.outputs.application-server-published == 'true' }}

permissions: {}

jobs:
Expand Down Expand Up @@ -74,31 +69,6 @@ jobs:
org.opencontainers.image.licenses=MIT
hephaestus.component=webapp

application-server-build:
name: "App Server"
if: >-
inputs.should_skip != 'true' &&
(inputs.application_server_changed == 'true' || github.event_name != 'pull_request')
uses: ./.github/workflows/reusable-docker-build.yml
permissions:
contents: read
packages: write
id-token: write
attestations: write
with:
image-name: "hephaestus-build/application-server"
# Application CDS configuration lives in server/application/pom.xml; rationale: docs/admin/buildpacks-cds-decision.md.
use-buildpacks: true
maven-module: "server/application"
registry: "ghcr.io"
single-arch: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
labels: |
org.opencontainers.image.title=Hephaestus Application Server
org.opencontainers.image.description=Spring Boot server for Hephaestus
org.opencontainers.image.vendor=AET TUM
org.opencontainers.image.licenses=MIT
hephaestus.component=application-server

agent-pi-build:
name: "Agent: Pi"
if: inputs.should_skip != 'true' && inputs.agent_images_changed == 'true'
Expand Down Expand Up @@ -161,8 +131,6 @@ jobs:
inputs.postgres_image_changed != 'true')
timeout-minutes: 15
runs-on: ubuntu-latest
outputs:
application-server-published: ${{ steps.tag.outputs.application-server-published }}
permissions:
packages: write
attestations: read
Expand Down Expand Up @@ -229,9 +197,6 @@ jobs:
}

[ "$WEBAPP_CHANGED" = "true" ] || tag_image webapp
if [ "$APPLICATION_SERVER_CHANGED" != "true" ]; then
tag_image application-server
echo "application-server-published=true" >> "$GITHUB_OUTPUT"
fi
[ "$APPLICATION_SERVER_CHANGED" = "true" ] || tag_image application-server
[ "$AGENT_IMAGES_CHANGED" = "true" ] || tag_image agent-pi
[ "$POSTGRES_IMAGE_CHANGED" = "true" ] || tag_image postgres
2 changes: 1 addition & 1 deletion .github/workflows/ci-profile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

- uses: ./.github/actions/setup-caches
with:
cache-type: application-server-integration
cache-type: application-server-reactor
os: ${{ runner.os }}

- name: Restore performance history
Expand Down
Loading
Loading