diff --git a/.github/workflows/rock-pull-request.yaml b/.github/workflows/rock-pull-request.yaml index eee00e9d..dc6a20e9 100644 --- a/.github/workflows/rock-pull-request.yaml +++ b/.github/workflows/rock-pull-request.yaml @@ -2,6 +2,12 @@ name: Pull Request on: workflow_call: + inputs: + rock-path: + description: "Path to the rock root" + required: false + default: "." + type: string jobs: @@ -10,23 +16,84 @@ jobs: runs-on: ubuntu-24.04 outputs: versions: ${{ steps.changed-versions.outputs.versions }} + is_monorepo: ${{ steps.detect-layout.outputs.is_monorepo }} + rock_path: ${{ steps.detect-layout.outputs.rock_path }} + just_dir: ${{ steps.detect-layout.outputs.just_dir }} + rock_name: ${{ steps.detect-layout.outputs.rock_name }} steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Detect rock layout + id: detect-layout + env: + ROCK_PATH_INPUT: ${{ inputs.rock-path }} + run: | + # Normalize the configured rock path. + rock_path="${ROCK_PATH_INPUT:-.}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi + + # If rock-path has a justfile, it's a single-rock repo. + # Otherwise treat it as a rock folder inside a monorepo and run just one level up. + if [[ -f "$rock_path/justfile" ]]; then + is_monorepo="false" + just_dir="$rock_path" + rock_name="" + else + is_monorepo="true" + just_dir="$(dirname "$rock_path")" + rock_name="$(basename "$rock_path")" + fi + + { + echo "rock_path=$rock_path" + echo "just_dir=$just_dir" + echo "rock_name=$rock_name" + echo "is_monorepo=$is_monorepo" + } >> "$GITHUB_OUTPUT" - name: Find rockcraft.yaml changes id: changed-files uses: tj-actions/changed-files@v46 with: - files: "**/rockcraft.yaml" + files: "${{ steps.detect-layout.outputs.rock_path }}/**/rockcraft.yaml" - name: Extract the versions id: changed-versions env: # CHANGED_FILES is a space-separated list CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + IS_MONOREPO: ${{ steps.detect-layout.outputs.is_monorepo }} + ROCK_PATH: ${{ steps.detect-layout.outputs.rock_path }} + ROCK_NAME: ${{ steps.detect-layout.outputs.rock_name }} run: | - versions="${CHANGED_FILES//\/rockcraft.yaml/}" # space-separated versions + versions="" + path_prefix="" + if [[ "$ROCK_PATH" != "." ]]; then + path_prefix="$ROCK_PATH/" + fi + # Convert changed rockcraft.yaml files into version tokens. + for file in $CHANGED_FILES; do + if [[ -n "$path_prefix" && "$file" != "$path_prefix"* ]]; then + continue + fi + relative="$file" + if [[ -n "$path_prefix" ]]; then + relative="${file#"$path_prefix"}" + fi + if [[ "$IS_MONOREPO" == "true" ]]; then + version="${relative%%/*}" + versions="$versions $version" + else + version="${relative%%/*}" + versions="$versions $version" + fi + done + versions="${versions# }" echo "versions=$versions" - echo "versions=$versions" >> "$GITHUB_OUTPUT" + { + echo "versions=$versions" + } >> "$GITHUB_OUTPUT" tests: name: Tests @@ -63,10 +130,22 @@ jobs: run: df -h - name: Pack and test the rocks run: | + rock_path="${{ needs.changes.outputs.just_dir }}" + is_monorepo="${{ needs.changes.outputs.is_monorepo }}" + rock_name="${{ needs.changes.outputs.rock_name }}" + cd "$rock_path" for version in ${{ needs.changes.outputs.versions }}; do - just pack "$version" - just test "$version" - just clean "$version" + if [[ "$is_monorepo" == "true" ]]; then + # Monorepo: pass rock name and version. + just pack "$rock_name" "$version" + just test "$rock_name" "$version" + just clean "$rock_name" "$version" + else + # Single rock: pass version only. + just pack "$version" + just test "$version" + just clean "$version" + fi done - name: Print disk utilization after packing if: ${{ (runner.debug == '1') }} diff --git a/.github/workflows/rock-release-dev.yaml b/.github/workflows/rock-release-dev.yaml index ae82e4f2..95f93f7e 100644 --- a/.github/workflows/rock-release-dev.yaml +++ b/.github/workflows/rock-release-dev.yaml @@ -7,6 +7,11 @@ on: description: "Name of the application for which to build the rock" required: true type: string + rock-path: + description: "Path to the rock root" + required: false + default: "." + type: string secrets: OBSERVABILITY_NOCTUA_TOKEN: required: true @@ -24,36 +29,85 @@ jobs: # Install Syft curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \ | sh -s -- -b /usr/local/bin + - name: Resolve rock context + id: rock-context + env: + ROCK_PATH_INPUT: ${{ inputs.rock-path }} + run: | + # Normalize the configured rock path. + rock_path="${ROCK_PATH_INPUT:-.}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi + + # Detect mode from the presence of a justfile in the provided rock path. + if [[ -f "$rock_path/justfile" ]]; then + is_monorepo="false" + just_dir="$rock_path" + rock_dir="$rock_path" + just_rock_name="" + else + is_monorepo="true" + rock_dir="$rock_path" + just_dir="$(dirname "$rock_path")" + just_rock_name="$(basename "$rock_path")" + fi + + # Resolve the latest version directory under the selected rock path. + latest_version="$(find "$rock_dir" -maxdepth 1 -type d -name '[0-9]*' | sort -V | tail -n1 | sed 's@.*/@@')" + if [[ -z "$latest_version" ]]; then + echo "No version directories found under $rock_dir" >&2 + exit 1 + fi + version_dir="$rock_dir/$latest_version" + # Export derived paths and mode for downstream steps. + { + echo "rock_path=$rock_path" + echo "just_dir=$just_dir" + echo "just_rock_name=$just_rock_name" + echo "rock_dir=$rock_dir" + echo "latest_version=$latest_version" + echo "version_dir=$version_dir" + echo "is_monorepo=$is_monorepo" + } >> "$GITHUB_OUTPUT" - name: Build rock + working-directory: ${{ steps.rock-context.outputs.just_dir }} + env: + JUST_ROCK_NAME: ${{ steps.rock-context.outputs.just_rock_name }} + IS_MONOREPO: ${{ steps.rock-context.outputs.is_monorepo }} + LATEST_VERSION: ${{ steps.rock-context.outputs.latest_version }} run: | - latest_version="$(find . -maxdepth 1 -type d -name '[0-9]*' | sort -V | tail -n1 | sed 's@./@@')" - just pack "$latest_version" + # Use rock-name + version for monorepos, otherwise version-only. + if [[ "$IS_MONOREPO" == "true" ]]; then + just pack "$JUST_ROCK_NAME" "$LATEST_VERSION" + else + just pack "$LATEST_VERSION" + fi - name: Upload rock to ghcr.io + working-directory: ${{ steps.rock-context.outputs.version_dir }} env: ROCK_NAME: ${{ inputs.rock-name }} OBSERVABILITY_NOCTUA_TOKEN: ${{ secrets.OBSERVABILITY_NOCTUA_TOKEN }} run: | - latest_version="$(find . -maxdepth 1 -type d -name '[0-9]*' | sort -V | tail -n1 | sed 's@./@@')" - cd "$latest_version" sudo skopeo --insecure-policy copy \ "oci-archive:$(realpath ./*.rock)" \ "docker://ghcr.io/canonical/$ROCK_NAME:dev" \ --dest-creds "observability-noctua-bot:$OBSERVABILITY_NOCTUA_TOKEN" - name: Create SBOM + working-directory: ${{ steps.rock-context.outputs.version_dir }} env: ROCK_NAME: ${{ inputs.rock-name }} run: | - latest_version="$(find . -maxdepth 1 -type d -name '[0-9]*' | sort -V | tail -n1 | sed 's@./@@')" - cd "$latest_version" # shellcheck disable=SC2086 # glob passed to realpath can't be wrapped syft "$(realpath ./${ROCK_NAME}_*.rock)" -o "spdx-json=${ROCK_NAME}.sbom.json" - name: Upload SBOM uses: actions/upload-artifact@v4 with: name: ${{ inputs.rock-name }}-sbom - path: "${{ inputs.rock-name}}.sbom.json" + path: "${{ steps.rock-context.outputs.version_dir }}/${{ inputs.rock-name}}.sbom.json" - name: Upload locally built rock artifact uses: actions/upload-artifact@v4 with: name: ${{ inputs.rock-name }}-rock - path: "${{ inputs.rock-name }}_*.rock" + path: "${{ steps.rock-context.outputs.version_dir }}/${{ inputs.rock-name }}_*.rock" diff --git a/.github/workflows/rock-release-oci-factory.yaml b/.github/workflows/rock-release-oci-factory.yaml index 17c4ed0e..d533e595 100644 --- a/.github/workflows/rock-release-oci-factory.yaml +++ b/.github/workflows/rock-release-oci-factory.yaml @@ -11,6 +11,11 @@ on: description: "Name of the application for which to build the rock" required: true type: string + rock-path: + description: "Path to the rock root" + required: false + default: "." + type: string risk-track: description: "Risk track on which to release the rock" required: false @@ -39,7 +44,7 @@ jobs: uses: tj-actions/changed-files@v46 with: path: rock - files: '**/rockcraft.yaml' + files: '${{ inputs.rock-path }}/**/rockcraft.yaml' - name: Sync the OCI Factory fork id: fork-sync if: steps.changed-files.outputs.all_changed_and_modified_files != '' @@ -64,16 +69,61 @@ jobs: shell: bash # The for-loop parsing is bash-specific env: ROCK_NAME: ${{ inputs.rock-name }} + ROCK_PATH_INPUT: ${{ inputs.rock-path }} # CHANGED_FILES is a space-separated list CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_and_modified_files }} RISK_TRACK: ${{ inputs.risk-track }} run: | TRACK=${RISK_TRACK:-stable} - cd rock + # Normalize the configured rock path. + rock_path="${ROCK_PATH_INPUT:-.}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi + + # If rock-path has a justfile, it's a single-rock repo. + # Otherwise treat it as a rock folder inside a monorepo. + if [[ -f "rock/$rock_path/justfile" ]]; then + is_monorepo="false" + else + is_monorepo="true" + fi + + # All version folders are located under rock_path in both modes. + cd "rock/$rock_path" # Export current time to create a unique branch name for the fork echo "now_epoch=$(date -d now +%s)" >> "$GITHUB_OUTPUT" sha="$(git rev-parse HEAD)" - versions="${CHANGED_FILES//\/rockcraft.yaml/}" # space-separated versions + versions="" + path_prefix="" + if [[ "$rock_path" != "." ]]; then + path_prefix="$rock_path/" + fi + # Extract version numbers from changed rockcraft.yaml files. + for file in $CHANGED_FILES; do + if [[ -n "$path_prefix" && "$file" != "$path_prefix"* ]]; then + continue + fi + relative="$file" + if [[ -n "$path_prefix" ]]; then + relative="${file#"$path_prefix"}" + fi + if [[ "$is_monorepo" == "true" ]]; then + version="${relative%%/*}" + versions="$versions $version" + else + version="${relative%%/*}" + versions="$versions $version" + fi + done + versions="${versions# }" + if [[ -z "$versions" ]]; then + echo "No changed rockcraft.yaml files for $ROCK_NAME" >&2 + echo "has_versions=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "has_versions=true" >> "$GITHUB_OUTPUT" # Build the --version flags for noctua versions_flags="$(for v in $versions; do echo -n "--version=$v "; done)" # Generate the manifest via noctua @@ -87,7 +137,7 @@ jobs: > "$GITHUB_WORKSPACE/oci-factory/oci/$ROCK_NAME/image.yaml" - name: Commit to the fork id: fork-commit - if: steps.changed-files.outputs.all_changed_and_modified_files != '' + if: steps.update-releases.outputs.has_versions == 'true' uses: EndBug/add-and-commit@v9 with: add: 'oci/${{ inputs.rock-name }}/image.yaml' @@ -96,7 +146,7 @@ jobs: new_branch: 'update-${{ steps.update-releases.outputs.now_epoch }}' push: 'origin update-${{ steps.update-releases.outputs.now_epoch }} --force' - name: Open a PR from the fork to upstream - if: steps.changed-files.outputs.all_changed_and_modified_files != '' + if: steps.update-releases.outputs.has_versions == 'true' id: upstream-pr env: GH_TOKEN: "${{ secrets.OBSERVABILITY_NOCTUA_TOKEN }}" diff --git a/.github/workflows/rock-update.yaml b/.github/workflows/rock-update.yaml index 5dc00f78..14eeca56 100644 --- a/.github/workflows/rock-update.yaml +++ b/.github/workflows/rock-update.yaml @@ -12,6 +12,11 @@ on: description: "Name of the application for which to build the rock" required: true type: string + rock-path: + description: "Path to the rock root (relative to repository root)" + required: false + default: "." + type: string source-repo: description: "Repository of the source application in 'org/repo' form" required: true @@ -105,13 +110,20 @@ jobs: if: steps.latest-release.outputs.release != '' shell: bash run: | + # Normalize rock path and derive the upstream version. + rock_path="${{ inputs.rock-path }}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi source_tag="${{ steps.latest-release.outputs.release }}" version="${source_tag#v}" # Explicitly filter for specific rocks because we'd rather notice if a new rock has a different release schema version="${version#mimir-}" # Filter out opentelemetry-collector prefixes version=${version#"cmd/builder/v"} - if [ ! -f "$GITHUB_WORKSPACE/main/$version/rockcraft.yaml" ]; then + # Only continue if this version folder does not already exist. + if [ ! -f "$GITHUB_WORKSPACE/main/$rock_path/$version/rockcraft.yaml" ]; then echo "version=$version" >> "$GITHUB_OUTPUT" echo "release=${{steps.latest-release.outputs.release}}" >> "$GITHUB_OUTPUT" echo "New upstream release ${{steps.latest-release.outputs.release}} found" @@ -133,34 +145,52 @@ jobs: SOURCE_TAG: "${{ steps.check.outputs.release }}" VERSION: "${{ steps.check.outputs.version }}" run: | - latest_rockcraft_file="$(find "$GITHUB_WORKSPACE/main/" -name "rockcraft.yaml" | sort -V | tail -n1)" - cp -r "$(dirname "$latest_rockcraft_file")" "$GITHUB_WORKSPACE/main/$VERSION" + # Copy the latest rockcraft folder and update version/source-tag fields. + rock_path="${{ inputs.rock-path }}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi + latest_rockcraft_file="$(find "$GITHUB_WORKSPACE/main/$rock_path" -name "rockcraft.yaml" | sort -V | tail -n1)" + cp -r "$(dirname "$latest_rockcraft_file")" "$GITHUB_WORKSPACE/main/$rock_path/$VERSION" source_tag="$SOURCE_TAG" version="$VERSION" yq -i \ '.version = strenv(version) | .parts.${{ inputs.rock-name }}["source-tag"] = strenv(source_tag)' \ - "$GITHUB_WORKSPACE/main/$VERSION/rockcraft.yaml" + "$GITHUB_WORKSPACE/main/$rock_path/$VERSION/rockcraft.yaml" - name: Update the Go version if: inputs.check-go && steps.check.outputs.release != '' env: VERSION: "${{ steps.check.outputs.version }}" run: | + # Update the Go build snap to match the application source. + rock_path="${{ inputs.rock-path }}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi go_version="$(grep -Po "^go \K(\S+)" "$GITHUB_WORKSPACE/application-src/go.mod")" \ # Delete the Go dependency and add the updated one yq -i 'del(.parts.${{ inputs.rock-name }}.build-snaps.[] | select(. == "go/*"))' \ - "$GITHUB_WORKSPACE/main/$VERSION/rockcraft.yaml" + "$GITHUB_WORKSPACE/main/$rock_path/$VERSION/rockcraft.yaml" # Snap channels are named after major.minor only, so cut the go version to that format go_major_minor="$(echo "$go_version" | sed -E "s/([0-9]+\.[0-9]+).*/\1/")" go_v="$go_major_minor" yq -i \ '.parts.${{ inputs.rock-name }}.build-snaps += "go/"+strenv(go_v)+"/stable"' \ - "$GITHUB_WORKSPACE/main/$VERSION/rockcraft.yaml" + "$GITHUB_WORKSPACE/main/$rock_path/$VERSION/rockcraft.yaml" - name: Update other build dependencies if: steps.check.outputs.release != '' && inputs.update-script != '' env: VERSION: "${{ steps.check.outputs.version }}" run: | + # Expose paths for the custom update script. + rock_path="${{ inputs.rock-path }}" + rock_path="${rock_path%/}" + if [[ "$rock_path" == "" ]]; then + rock_path="." + fi export application_src="$GITHUB_WORKSPACE/application-src" - export rockcraft_yaml="$GITHUB_WORKSPACE/main/$VERSION/rockcraft.yaml" + export rockcraft_yaml="$GITHUB_WORKSPACE/main/$rock_path/$VERSION/rockcraft.yaml" cat > update-script.sh << EOF ${{ inputs.update-script }} EOF