diff --git a/.github/actions/slack-build-notify/action.yml b/.github/actions/slack-build-notify/action.yml index efccfdecc..55ffb0eff 100644 --- a/.github/actions/slack-build-notify/action.yml +++ b/.github/actions/slack-build-notify/action.yml @@ -54,32 +54,36 @@ runs: .filter(r => r.id !== context.runId && r.conclusion !== 'cancelled') .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - if (previousRuns.length === 0) { - core.info('No previous completed runs found, skipping notification'); - return; - } - - const previousRun = previousRuns[0]; const currentFailed = currentResult !== 'success'; - // Compare against the previous run's CI job conclusion (not the workflow - // conclusion) so that clean-job flaps don't trigger false notifications. - // Falls back to workflow conclusion if the CI job isn't found. - const { data: { jobs: prevJobs } } = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: previousRun.id, - per_page: 100, - }); - const prevCiJob = prevJobs.find(j => j.name === 'CI'); - const previousConclusion = prevCiJob - ? prevCiJob.conclusion - : previousRun.conclusion; - const previousFailed = previousConclusion !== 'success'; - - if (currentFailed === previousFailed) { - core.info(`No state transition (current: ${currentResult}, previous: ${previousConclusion}), skipping`); - return; + if (previousRuns.length === 0) { + if (!currentFailed) { + core.info('No previous completed runs found and build passed, skipping notification'); + return; + } + // No history — notify on failure directly (e.g. merge queue runs on ephemeral refs) + } else { + const previousRun = previousRuns[0]; + + // Compare against the previous run's CI job conclusion (not the workflow + // conclusion) so that clean-job flaps don't trigger false notifications. + // Falls back to workflow conclusion if the CI job isn't found. + const { data: { jobs: prevJobs } } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: previousRun.id, + per_page: 100, + }); + const prevCiJob = prevJobs.find(j => j.name === 'CI'); + const previousConclusion = prevCiJob + ? prevCiJob.conclusion + : previousRun.conclusion; + const previousFailed = previousConclusion !== 'success'; + + if (currentFailed === previousFailed) { + core.info(`No state transition (current: ${currentResult}, previous: ${previousConclusion}), skipping`); + return; + } } const runUrl = currentRun.html_url; diff --git a/.github/workflows/bakery-build-pr.yml b/.github/workflows/bakery-build-pr.yml index 1cfe51ad3..40ccd5f67 100644 --- a/.github/workflows/bakery-build-pr.yml +++ b/.github/workflows/bakery-build-pr.yml @@ -35,6 +35,11 @@ on: default: "exclude" required: false type: string + latest-only: + description: "Build only the latest version of each image (faster PR checks)" + default: false + required: false + type: boolean retry: description: "Number of times to retry a failed build" default: 1 @@ -102,9 +107,12 @@ jobs: IS_FORK: ${{ needs.detect.outputs.is-fork }} DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + LATEST_ONLY: ${{ inputs.latest-only }} BAKERY_CONTEXT: ${{ inputs.context }} run: | - FULL_MATRIX=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --context "$BAKERY_CONTEXT" | jq --compact-output .) + LATEST_FLAGS=() + if [ "$LATEST_ONLY" = "true" ]; then LATEST_FLAGS=(--latest); fi + FULL_MATRIX=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" "${LATEST_FLAGS[@]}" --context "$BAKERY_CONTEXT" | jq --compact-output .) if [ "$IS_FORK" = "true" ]; then # Skip arm64 for fork PRs — paid runners may not be available FULL_MATRIX=$(echo "$FULL_MATRIX" | jq --compact-output '[.[] | select(.platform != "linux/arm64")]') @@ -116,9 +124,12 @@ jobs: env: DEV_VERSIONS: ${{ inputs.dev-versions }} MATRIX_VERSIONS: ${{ inputs.matrix-versions }} + LATEST_ONLY: ${{ inputs.latest-only }} BAKERY_CONTEXT: ${{ inputs.context }} run: | - result=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" --exclude platform --context "$BAKERY_CONTEXT") + LATEST_FLAGS=() + if [ "$LATEST_ONLY" = "true" ]; then LATEST_FLAGS=(--latest); fi + result=$(bakery ci matrix --quiet --dev-versions "$DEV_VERSIONS" --matrix-versions "$MATRIX_VERSIONS" "${LATEST_FLAGS[@]}" --exclude platform --context "$BAKERY_CONTEXT") echo "versions_matrix=$(echo "$result" | jq --compact-output .)" >> "$GITHUB_OUTPUT" build-test: diff --git a/posit-bakery/posit_bakery/cli/ci.py b/posit-bakery/posit_bakery/cli/ci.py index d0b5e3511..b4da5e378 100644 --- a/posit-bakery/posit_bakery/cli/ci.py +++ b/posit-bakery/posit_bakery/cli/ci.py @@ -75,6 +75,14 @@ def matrix( rich_help_panel=RichHelpPanelEnum.FILTERS, ), ] = None, + latest: Annotated[ + Optional[bool], + typer.Option( + "--latest", + help="Include only the latest version of each image. For matrix images, includes only the highest-version combination.", + rich_help_panel=RichHelpPanelEnum.FILTERS, + ), + ] = False, exclude: Annotated[ Optional[list[BakeryCIMatrixFieldEnum]], typer.Option(help="Fields to exclude splitting the matrix by."), @@ -152,6 +160,8 @@ def matrix( continue if image_version is not None and not version_matches(ver.name, image_version): continue + if latest and not ver.latest: + continue if BakeryCIMatrixFieldEnum.VERSION not in exclude: entry["version"] = ver.name diff --git a/posit-bakery/test/cli/testdata/ci/matrix/multiversion/default.json b/posit-bakery/test/cli/testdata/ci/matrix/multiversion/default.json new file mode 100644 index 000000000..699c5c863 --- /dev/null +++ b/posit-bakery/test/cli/testdata/ci/matrix/multiversion/default.json @@ -0,0 +1 @@ +[{"image": "test-image", "version": "2.0.0", "dev": false, "platform": "linux/amd64"}, {"image": "test-image", "version": "1.0.0", "dev": false, "platform": "linux/amd64"}] diff --git a/posit-bakery/test/cli/testdata/ci/matrix/multiversion/latest_only.json b/posit-bakery/test/cli/testdata/ci/matrix/multiversion/latest_only.json new file mode 100644 index 000000000..fcd48eec6 --- /dev/null +++ b/posit-bakery/test/cli/testdata/ci/matrix/multiversion/latest_only.json @@ -0,0 +1 @@ +[{"image": "test-image", "version": "2.0.0", "dev": false, "platform": "linux/amd64"}] diff --git a/posit-bakery/test/features/cli/ci/matrix.feature b/posit-bakery/test/features/cli/ci/matrix.feature index 243a794ba..c6d4fa156 100644 --- a/posit-bakery/test/features/cli/ci/matrix.feature +++ b/posit-bakery/test/features/cli/ci/matrix.feature @@ -47,3 +47,19 @@ Feature: matrix | --image-version | 9.9.9 | When I execute the command Then The command fails + + Scenario: Generating a full CI matrix for the multiversion suite + Given I call bakery ci matrix + * in the multiversion context + When I execute the command + Then The command succeeds + * the matrix matches testdata ci/matrix/multiversion/default.json + + Scenario: Filtering the CI matrix to latest versions only + Given I call bakery ci matrix + * in the multiversion context + * with the arguments: + | --latest | | + When I execute the command + Then The command succeeds + * the matrix matches testdata ci/matrix/multiversion/latest_only.json diff --git a/posit-bakery/test/resources/multiversion/bakery.yaml b/posit-bakery/test/resources/multiversion/bakery.yaml new file mode 100644 index 000000000..a7a2d91bb --- /dev/null +++ b/posit-bakery/test/resources/multiversion/bakery.yaml @@ -0,0 +1,23 @@ +repository: + url: "github.com/posit-dev/images-shared" + vendor: "Posit Software, PBC" + maintainer: + name: "Posit Docker Team" + email: "docker@posit.co" + +registries: + - host: "ghcr.io" + namespace: "posit-dev" + +images: + - name: "test-image" + versions: + - name: "2.0.0" + latest: true + os: + - name: Ubuntu 22.04 + primary: true + - name: "1.0.0" + os: + - name: Ubuntu 22.04 + primary: true