diff --git a/.github/workflows/gradle-build-test.yaml b/.github/workflows/gradle-build-test.yaml index a62c025..bfa0659 100644 --- a/.github/workflows/gradle-build-test.yaml +++ b/.github/workflows/gradle-build-test.yaml @@ -110,7 +110,7 @@ on: required: false type: string # yamllint disable-line rule:line-length - default: 'lfreleng-actions//.github/harden-runner/lfreleng-actions/allow_list.txt@cc89ca0f02a1c8fd875c9758896f0929097d25b7' # v0.12.0 + default: 'lfreleng-actions//.github/harden-runner/lfreleng-actions/allow_list.txt@bf6642f68d58c1b81bbe993e676d6cc339ac3654' # v0.12.2 # --- Job timeouts (minutes) --- # Sized per job rather than uniformly. The build, test-report and @@ -141,6 +141,29 @@ on: type: number default: 30 + # --- Build artefact publication --- + # Off by default. The build's packaged output is large and most + # verify lanes never look at it again, so uploading unconditionally + # would impose storage on every consumer to serve a minority. Turn + # it on where a later job, or a human, needs the built output: + # publish lanes, container builds, or downloading a jar from a + # failed run to reproduce a problem locally. + upload_build_artifacts: + description: >- + Upload the packaged build output (jar/war/ear from each + module's build/libs directory) as a workflow artefact. + required: false + type: boolean + default: false + build_artifact_retention_days: + description: >- + Retention for the build artefact. Shorter than the SBOM's 45 + days: build output is reproducible from the commit, so it is + a convenience rather than a record. + required: false + type: number + default: 7 + # --- Gerrit-aware checkout (empty unless dispatched via Gerrit) --- gerrit_refspec: description: 'Gerrit refspec of the change under test' @@ -326,16 +349,16 @@ jobs: # Detect Java/Gradle project metadata: Java version (including # sourceCompatibility/targetCompatibility and toolchain settings), - # module layout, version.properties and any releases/*.yaml. - # Surfaces a metadata summary and lets the build pick the Java - # version when the caller does not pin one. + # module layout, version.properties and any releases/*.yaml. Lets + # the build pick the Java version when the caller does not pin + # one. The action takes no github_summary input; one was passed + # here until it was noticed in the run annotations. - name: 'Detect build metadata' id: metadata # yamllint disable-line rule:line-length uses: lfreleng-actions/build-metadata-action@6c432669e6dec2949442da27d0aa270f2bca0ecb # v0.7.1 with: path_prefix: ${{ inputs.path_prefix }} - github_summary: 'true' - name: 'Build with Gradle' id: build @@ -384,6 +407,53 @@ jobs: path: junit-reports if-no-files-found: warn + # Gradle has no m2repo equivalent in a plain build, so there is + # one shape here: whatever each module packaged into build/libs. + - name: 'Collect build artefacts' + id: build-artifacts + if: ${{ always() && inputs.upload_build_artifacts }} + shell: bash + env: + PATH_PREFIX: ${{ inputs.path_prefix }} + run: | + # Stage the build output under a single directory for upload. + set -euo pipefail + dest="build-artifacts" + count=0 + + while IFS= read -r -d '' dir; do + for f in "${dir}"/*.jar "${dir}"/*.war "${dir}"/*.ear; do + [ -f "${f}" ] || continue + rel="${f#"${PATH_PREFIX}/"}" + mkdir -p "${dest}/$(dirname "${rel}")" + cp "${f}" "${dest}/${rel}" + count=$((count + 1)) + done + done < <(find "${PATH_PREFIX}" -type d -path '*/build/libs' -print0) + + echo "count=${count}" >> "$GITHUB_OUTPUT" + + { + echo "### Build artefacts" + echo "" + echo "| Property | Value |" + echo "| -------- | ----- |" + echo "| Files | ${count} |" + } >> "$GITHUB_STEP_SUMMARY" + + echo "Collected ${count} file(s) from build/libs" + + - name: 'Upload build artefacts' + # yamllint disable-line rule:line-length + if: ${{ always() && inputs.upload_build_artifacts && steps.build-artifacts.outputs.count != '0' }} + # yamllint disable-line rule:line-length + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: gradle-build-artifacts + path: build-artifacts + retention-days: ${{ inputs.build_artifact_retention_days }} + if-no-files-found: error + tests: name: 'Tests' runs-on: ubuntu-latest diff --git a/.github/workflows/maven-build-test.yaml b/.github/workflows/maven-build-test.yaml index c75775a..172d768 100644 --- a/.github/workflows/maven-build-test.yaml +++ b/.github/workflows/maven-build-test.yaml @@ -122,7 +122,7 @@ on: required: false type: string # yamllint disable-line rule:line-length - default: 'lfreleng-actions//.github/harden-runner/lfreleng-actions/allow_list.txt@cc89ca0f02a1c8fd875c9758896f0929097d25b7' # v0.12.0 + default: 'lfreleng-actions//.github/harden-runner/lfreleng-actions/allow_list.txt@bf6642f68d58c1b81bbe993e676d6cc339ac3654' # v0.12.2 # --- Job timeouts (minutes) --- # Sized per job rather than uniformly. The build, test-report and @@ -153,6 +153,31 @@ on: type: number default: 30 + # --- Build artefact publication --- + # Off by default. The build's packaged output is large and most + # verify lanes never look at it again, so uploading unconditionally + # would impose storage on every consumer to serve a minority. Turn + # it on where a later job, or a human, needs the built output: + # publish/stage lanes, container builds, CSIT, or downloading a jar + # from a failed run to reproduce a problem locally. + upload_build_artifacts: + description: >- + Upload the packaged build output as a workflow artefact. + Prefers the m2repo when a deploy phase produced one, since + that is the layout publish and stage lanes consume; otherwise + takes the reactor's packaged output. + required: false + type: boolean + default: false + build_artifact_retention_days: + description: >- + Retention for the build artefact. Shorter than the SBOM's 45 + days: build output is reproducible from the commit, so it is + a convenience rather than a record. + required: false + type: number + default: 7 + # --- Gerrit-aware checkout (empty unless dispatched via Gerrit) --- gerrit_refspec: description: 'Gerrit refspec of the change under test' @@ -299,6 +324,11 @@ jobs: coverage: ${{ steps.build.outputs.coverage }} branch_coverage: ${{ steps.build.outputs.branch_coverage }} reports_found: ${{ steps.reports.outputs.found }} + # Surfaced so a caller's downstream job can locate the + # publish-ready repository layout without re-deriving the path. + m2repo_exists: ${{ steps.build.outputs.m2repo_exists }} + m2repo_path: ${{ steps.build.outputs.m2repo_path }} + artifact_count: ${{ steps.build.outputs.artifact_count }} permissions: contents: read timeout-minutes: ${{ inputs.build_timeout_minutes }} @@ -349,16 +379,16 @@ jobs: # Detect Java/Maven project metadata: Java version (including # maven.compiler.release plus parent/reactor resolution), module - # layout, version.properties and any releases/*.yaml. Surfaces a - # metadata summary and lets the build pick the Java version when - # the caller does not pin one. + # layout, version.properties and any releases/*.yaml. Lets the + # build pick the Java version when the caller does not pin one. + # The action takes no github_summary input; one was passed here + # until it was noticed in the run annotations, ignored throughout. - name: 'Detect build metadata' id: metadata # yamllint disable-line rule:line-length uses: lfreleng-actions/build-metadata-action@6c432669e6dec2949442da27d0aa270f2bca0ecb # v0.7.1 with: path_prefix: ${{ inputs.path_prefix }} - github_summary: 'true' - name: 'Build with Maven' id: build @@ -411,6 +441,86 @@ jobs: path: junit-reports if-no-files-found: warn + # Two shapes can come out of a Maven build and they are not + # interchangeable. A deploy phase writes a repository layout to + # m2repo (groupId/artifactId/version/...), which is what Nexus + # staging and publish consume. Any other phase leaves packaged + # output scattered through the reactor's target/ directories. + # Prefer the former when it exists and say which was taken, so a + # consumer downloading the artefact knows what it has. + - name: 'Collect build artefacts' + id: build-artifacts + if: ${{ always() && inputs.upload_build_artifacts }} + shell: bash + env: + PATH_PREFIX: ${{ inputs.path_prefix }} + M2REPO_EXISTS: ${{ steps.build.outputs.m2repo_exists }} + M2REPO_PATH: ${{ steps.build.outputs.m2repo_path }} + run: | + # Stage the build output under a single directory for upload. + set -euo pipefail + dest="build-artifacts" + shape="none" + count=0 + + # Take the location from the build action rather than + # re-deriving it, so the layout stays the action's business. + if [ "${M2REPO_EXISTS}" = "true" ] && [ -n "${M2REPO_PATH}" ] \ + && [ -d "${M2REPO_PATH}" ]; then + shape="m2repo" + mkdir -p "${dest}" + cp -R "${M2REPO_PATH}" "${dest}/m2repo" + count=$(find "${dest}/m2repo" -type f \ + \( -name '*.jar' -o -name '*.war' -o -name '*.ear' \ + -o -name '*.pom' \) | wc -l | tr -d ' ') + else + # No deploy phase ran. Take the reactor's packaged output: + # files sitting directly in a module's target/ directory, + # not the compiled classes or unpacked dependencies below it. + shape="reactor" + while IFS= read -r -d '' dir; do + for f in "${dir}"/*.jar "${dir}"/*.war "${dir}"/*.ear; do + [ -f "${f}" ] || continue + rel="${f#"${PATH_PREFIX}/"}" + mkdir -p "${dest}/$(dirname "${rel}")" + cp "${f}" "${dest}/${rel}" + count=$((count + 1)) + done + done < <(find "${PATH_PREFIX}" -type d -name target -print0) + fi + + if [ "${count}" -eq 0 ]; then + shape="none" + fi + + { + echo "shape=${shape}" + echo "count=${count}" + } >> "$GITHUB_OUTPUT" + + { + echo "### Build artefacts" + echo "" + echo "| Property | Value |" + echo "| -------- | ----- |" + echo "| Layout | ${shape} |" + echo "| Files | ${count} |" + } >> "$GITHUB_STEP_SUMMARY" + + echo "Collected ${count} file(s) as '${shape}' layout" + + - name: 'Upload build artefacts' + # yamllint disable-line rule:line-length + if: ${{ always() && inputs.upload_build_artifacts && steps.build-artifacts.outputs.count != '0' }} + # yamllint disable-line rule:line-length + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + # yamllint disable-line rule:line-length + name: maven-build-artifacts-${{ steps.build-artifacts.outputs.shape }} + path: build-artifacts + retention-days: ${{ inputs.build_artifact_retention_days }} + if-no-files-found: error + tests: name: 'Tests' runs-on: ubuntu-latest diff --git a/docs/BRIEF.md b/docs/BRIEF.md index 302b4e6..609746f 100644 --- a/docs/BRIEF.md +++ b/docs/BRIEF.md @@ -73,6 +73,32 @@ gerrit-validate ─┬─ repository-metadata (informational) `*/target/*-reports/*.xml`, copies them under `junit-reports/`, and sets `found`. 4. When reports exist, they upload as the `maven-junit-reports` artefact. +5. When `upload_build_artifacts` is set, a "Collect build artefacts" + step stages the packaged output and uploads it. + +Build artefact publication is **opt-in**. The packaged output is large +and most verify lanes never read it back, so uploading it unconditionally +would charge storage to every consumer in order to serve a minority. The +cases that want it are real, though — publish and stage lanes, container +builds, CSIT, and a human pulling a jar out of a failed run — so the +workflow makes it a switch rather than omitting it. + +Two shapes can come out of a Maven build, and they are not +interchangeable. A `deploy` phase writes a repository layout to `m2repo` +(`groupId/artifactId/version/...`), which is what Nexus staging and +`nexus-publish-action` consume; any other phase leaves packaged output +scattered through the reactor's `target/` directories. The collect step +prefers the former when it exists and records which it took in the +artefact name (`maven-build-artifacts-m2repo` or +`maven-build-artifacts-reactor`), so a consumer knows what it has +without inspecting the contents. Note the lane's default `mvn_phases` is +`clean install`, which produces the reactor shape; `m2repo` appears only +when a caller asks for `deploy`. The build job also surfaces +`m2repo_exists`, `m2repo_path` and `artifact_count` as job outputs. + +Gradle has no `m2repo` equivalent in a plain build, so its lane collects +one shape — each module's `build/libs` output — and names the artefact +`gradle-build-artifacts`. A global `settings.xml` (which commonly carries Nexus server credentials) is never accepted as a plain input: the workflow declares a diff --git a/examples/gradle/build-test/github.yaml b/examples/gradle/build-test/github.yaml index dfdef93..7f2c30f 100644 --- a/examples/gradle/build-test/github.yaml +++ b/examples/gradle/build-test/github.yaml @@ -55,3 +55,9 @@ jobs: # tests_timeout_minutes: 30 # test report rendering # sbom_timeout_minutes: 30 # SBOM generation # grype_timeout_minutes: 30 # Grype scan + # + # Keep the packaged build output from each module's build/libs + # directory. Off by default; turn it on where a later job or a + # human needs the jars. + # upload_build_artifacts: true + # build_artifact_retention_days: 7 diff --git a/examples/maven/build-test/github.yaml b/examples/maven/build-test/github.yaml index df2bb1d..0594dd3 100644 --- a/examples/maven/build-test/github.yaml +++ b/examples/maven/build-test/github.yaml @@ -57,6 +57,12 @@ jobs: # tests_timeout_minutes: 30 # test report rendering # sbom_timeout_minutes: 30 # SBOM generation # grype_timeout_minutes: 30 # Grype scan + # + # Keep the packaged build output. Off by default; turn it on where a + # later job or a human needs the jars. Prefers the m2repo layout + # when a deploy phase produced one, else the reactor's target/ jars. + # upload_build_artifacts: true + # build_artifact_retention_days: 7 # settings.xml can carry server credentials, so it is passed only as a # masked secret (never a plain input). Synthesise it with # maven-xml-settings-action and forward it here when your build needs a