* ci: remove automatic test gates and trim test suites * docs: remove CI audit report
408 lines
15 KiB
YAML
408 lines
15 KiB
YAML
name: CI
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened, ready_for_review]
|
|
branches: ["**"]
|
|
pull_request_target:
|
|
types: [closed]
|
|
branches: ["**"]
|
|
push:
|
|
branches: [main, dev, beta, next, v2, release/v2]
|
|
merge_group:
|
|
workflow_dispatch:
|
|
inputs:
|
|
operation:
|
|
description: Operation to run
|
|
required: true
|
|
default: validate
|
|
type: choice
|
|
options:
|
|
- validate
|
|
- publish
|
|
- cleanup
|
|
pr_number:
|
|
description: PR number for publish or cleanup
|
|
required: false
|
|
type: number
|
|
ref:
|
|
description: Git ref to validate or publish
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
# Every event for a PR shares one group so a close event cancels stale image
|
|
# publication and a newer synchronize run cannot be overwritten by an older run.
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' || github.ref != 'refs/heads/main' }}
|
|
|
|
env:
|
|
FORCE_COLOR: 3
|
|
SKIP_ENV_VALIDATION: true
|
|
TURBO_TELEMETRY_DISABLED: 1
|
|
REGISTRY: ghcr.io
|
|
PREVIEW_IMAGE: ghcr.io/homarr-labs/homarr-test
|
|
|
|
jobs:
|
|
fast-gate:
|
|
name: Fast gate
|
|
if: github.event_name != 'pull_request_target' && (github.event_name != 'workflow_dispatch' || inputs.operation != 'cleanup')
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
env:
|
|
TURBO_API: ${{ secrets.TURBO_API }}
|
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
|
|
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || 'HEAD~1' }}
|
|
TURBO_SCM_HEAD: ${{ github.sha }}
|
|
TURBO_PLATFORM: linux/amd64
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
if: "!env.ACT"
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
|
|
|
- name: Validate manual inputs
|
|
if: github.event_name == 'workflow_dispatch' && inputs.operation == 'publish'
|
|
env:
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
BUILD_REF: ${{ inputs.ref }}
|
|
run: |
|
|
test -n "$PR_NUMBER" || { echo "pr_number is required for publish"; exit 1; }
|
|
test -n "$BUILD_REF" || { echo "ref is required for publish"; exit 1; }
|
|
|
|
- name: Setup
|
|
uses: ./tooling/github/setup
|
|
|
|
- name: Copy environment defaults
|
|
run: cp .env.example .env
|
|
|
|
- name: Lint, typecheck, and build affected workspaces
|
|
run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful --concurrency=2
|
|
|
|
- name: Validate workspace configuration
|
|
run: pnpm lint:ws
|
|
|
|
- name: Validate Custom Widget architecture
|
|
run: pnpm check:custom-widgets
|
|
|
|
- name: Validate checked-in OpenAPI schema
|
|
run: pnpm check:openapi
|
|
|
|
- name: Validate Custom Widget bundle budget
|
|
run: |
|
|
pnpm turbo run build --filter=@homarr/nextjs
|
|
pnpm check:custom-widget-bundle
|
|
|
|
container:
|
|
name: Container build
|
|
# Same-repository previews already build amd64; only build a fallback when no digest is available.
|
|
needs: [fast-gate, preview-amd64]
|
|
if: |
|
|
!cancelled() && needs.fast-gate.result == 'success' &&
|
|
needs.preview-amd64.result != 'failure' && needs.preview-amd64.outputs.digest == '' &&
|
|
(
|
|
github.event_name == 'merge_group' ||
|
|
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
|
|
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
|
(github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish'))
|
|
)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
if: "!env.ACT"
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
|
|
|
- name: Set up Docker Buildx
|
|
if: needs.preview-amd64.outputs.digest == '' && !env.ACT
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Build container once
|
|
if: needs.preview-amd64.outputs.digest == '' && !env.ACT
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
network: host
|
|
platforms: linux/amd64
|
|
load: true
|
|
push: false
|
|
tags: homarr-ci
|
|
labels: |
|
|
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
org.opencontainers.image.ref.name=pr-${{ github.event.pull_request.number || inputs.pr_number || 'none' }}
|
|
cache-from: type=gha,scope=homarr-e2e
|
|
cache-to: type=gha,scope=homarr-e2e,mode=max
|
|
secrets: |
|
|
TURBO_API=${{ secrets.TURBO_API }}
|
|
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
|
|
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
|
|
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
|
|
|
|
- name: Build container once with Act
|
|
if: env.ACT
|
|
env:
|
|
TURBO_API: ${{ secrets.TURBO_API }}
|
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
|
|
run: |
|
|
secret_args=()
|
|
for secret in TURBO_API TURBO_TEAM TURBO_TOKEN TURBO_REMOTE_CACHE_SIGNATURE_KEY; do
|
|
if [ -n "${!secret:-}" ]; then
|
|
secret_args+=(--secret "id=${secret},env=${secret}")
|
|
fi
|
|
done
|
|
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \
|
|
--tag homarr-ci "${secret_args[@]}" .
|
|
|
|
preview-amd64:
|
|
name: Build amd64 preview image
|
|
needs: fast-gate
|
|
if: |
|
|
(
|
|
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
|
|
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
|
|
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
|
|
)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
outputs:
|
|
digest: ${{ steps.preview-build.outputs.digest }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
if: "!env.ACT"
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
|
|
|
- name: Log in to GHCR
|
|
if: "!env.ACT"
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Set up Docker Buildx
|
|
if: "!env.ACT"
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Build and push amd64 preview image by digest
|
|
id: preview-build
|
|
if: "!env.ACT"
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
network: host
|
|
platforms: linux/amd64
|
|
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
|
cache-from: type=gha,scope=homarr-linux-amd64
|
|
cache-to: type=gha,scope=homarr-linux-amd64,mode=max
|
|
secrets: |
|
|
TURBO_API=${{ secrets.TURBO_API }}
|
|
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
|
|
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
|
|
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
|
|
|
|
preview-arm64:
|
|
name: Build arm64 preview image
|
|
needs: fast-gate
|
|
if: |
|
|
(
|
|
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
|
|
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
|
|
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
|
|
)
|
|
runs-on: ubuntu-24.04-arm
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
outputs:
|
|
digest: ${{ steps.preview-build.outputs.digest }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
if: "!env.ACT"
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
|
|
|
- name: Log in to GHCR
|
|
if: "!env.ACT"
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Set up Docker Buildx
|
|
if: "!env.ACT"
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Build and push arm64 preview image by digest
|
|
id: preview-build
|
|
if: "!env.ACT"
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
network: host
|
|
platforms: linux/arm64
|
|
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
|
cache-from: type=gha,scope=homarr-linux-arm64
|
|
cache-to: type=gha,scope=homarr-linux-arm64,mode=max
|
|
secrets: |
|
|
TURBO_API=${{ secrets.TURBO_API }}
|
|
TURBO_TEAM=${{ secrets.TURBO_TEAM }}
|
|
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
|
|
TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
|
|
|
|
publish-preview:
|
|
name: Publish multi-platform preview image
|
|
needs: [fast-gate, preview-amd64, preview-arm64]
|
|
if: |
|
|
!cancelled() &&
|
|
needs.fast-gate.result == 'success' &&
|
|
needs.preview-amd64.result == 'success' &&
|
|
needs.preview-arm64.result == 'success' &&
|
|
(
|
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
|
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
|
|
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
|
|
)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
pull-requests: read
|
|
env:
|
|
PREVIEW_TAG: ${{ github.event_name == 'push' && 'v2' || format('pr-{0}', github.event.pull_request.number || inputs.pr_number) }}
|
|
steps:
|
|
- name: Log in to GHCR
|
|
if: "!env.ACT"
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Set up Docker Buildx
|
|
if: "!env.ACT"
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Verify PR head before publication
|
|
if: github.event_name == 'pull_request'
|
|
uses: actions/github-script@v8
|
|
env:
|
|
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
with:
|
|
script: |
|
|
const { data: pull } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: context.payload.pull_request.number,
|
|
});
|
|
if (pull.state !== "open") {
|
|
core.setFailed(`PR #${pull.number} is ${pull.state}; refusing to publish its preview.`);
|
|
return;
|
|
}
|
|
if (pull.head.sha !== process.env.EXPECTED_HEAD_SHA) {
|
|
core.setFailed(`PR #${pull.number} moved from ${process.env.EXPECTED_HEAD_SHA} to ${pull.head.sha}.`);
|
|
}
|
|
|
|
- name: Publish preview manifest
|
|
if: "!env.ACT"
|
|
env:
|
|
AMD64_DIGEST: ${{ needs.preview-amd64.outputs.digest }}
|
|
ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }}
|
|
run: |
|
|
tags=(-t "${PREVIEW_IMAGE}:${PREVIEW_TAG}")
|
|
docker buildx imagetools create "${tags[@]}" \
|
|
"${PREVIEW_IMAGE}@${AMD64_DIGEST}" \
|
|
"${PREVIEW_IMAGE}@${ARM64_DIGEST}"
|
|
|
|
- name: Verify preview manifest platforms
|
|
if: "!env.ACT"
|
|
run: |
|
|
tags=("${PREVIEW_TAG}")
|
|
for tag in "${tags[@]}"; do
|
|
image="${PREVIEW_IMAGE}:${tag}"
|
|
manifest="$(docker buildx imagetools inspect "$image" --raw)"
|
|
for architecture in amd64 arm64; do
|
|
jq --exit-status --arg architecture "$architecture" \
|
|
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
|
|
<<<"$manifest" >/dev/null
|
|
done
|
|
docker buildx imagetools inspect "$image"
|
|
done
|
|
|
|
cleanup:
|
|
name: Clean up preview image
|
|
if: github.event_name == 'pull_request_target' || (github.event_name == 'workflow_dispatch' && inputs.operation == 'cleanup')
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
packages: write
|
|
env:
|
|
IMAGE_TAG: pr-${{ github.event.pull_request.number || inputs.pr_number }}
|
|
steps:
|
|
- name: Validate cleanup input
|
|
if: github.event_name == 'workflow_dispatch'
|
|
env:
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
run: test -n "$PR_NUMBER" || { echo "pr_number is required for cleanup"; exit 1; }
|
|
|
|
- name: Delete GHCR package version
|
|
if: "!env.ACT"
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const packageName = "homarr-test";
|
|
const org = context.repo.owner;
|
|
const tag = process.env.IMAGE_TAG;
|
|
const versions = await github.paginate(
|
|
github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg,
|
|
{ package_type: "container", package_name: packageName, org, per_page: 100 },
|
|
);
|
|
const target = versions.find((version) =>
|
|
version.metadata?.container?.tags?.includes(tag),
|
|
);
|
|
if (!target) {
|
|
core.info(`No package version found with tag "${tag}"`);
|
|
return;
|
|
}
|
|
const targetTags = target.metadata?.container?.tags ?? [];
|
|
const extraTags = targetTags.filter((targetTag) => targetTag !== tag);
|
|
if (extraTags.length > 0) {
|
|
core.warning(
|
|
`Package version ${target.id} also carries tag(s) ${extraTags.map((targetTag) => `"${targetTag}"`).join(", ")}; refusing cleanup.`,
|
|
);
|
|
return;
|
|
}
|
|
await github.rest.packages.deletePackageVersionForOrg({
|
|
package_type: "container",
|
|
package_name: packageName,
|
|
org,
|
|
package_version_id: target.id,
|
|
});
|
|
core.info(`Deleted package version ${target.id} (${tag})`);
|
|
|
|
- name: Show Act cleanup result
|
|
if: env.ACT
|
|
run: echo "Act validated cleanup for ${IMAGE_TAG}; external GHCR writes are disabled locally."
|