diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e7c85c..d9f994a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,8 +12,19 @@ concurrency: jobs: build: - name: 'Check, Build, Test, Publish DevContainer' - runs-on: ubuntu-latest + strategy: + matrix: + os: [arm64, amd64] + include: + - os: amd64 + name: 'DevContainer (amd64)' + runner: ubuntu-24.04 + - os: arm64 + name: 'DevContainer (arm64)' + runner: ubuntu-24.04-arm + + name: '${{ matrix.name }}' + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write @@ -38,6 +49,8 @@ jobs: # We want to only use it for building and testing the actual container, which resides in src/s-core-devcontainer. push: "never" runCmd: | + set -eux pipefail + # Check pre-commit run --show-diff-on-failure --color=always --all-files || exit -1 @@ -45,7 +58,7 @@ jobs: ./scripts/create_builder.sh # Build - ./scripts/build.sh + ./scripts/build.sh --${{ matrix.os }} "main" # Test ./scripts/test.sh @@ -56,5 +69,43 @@ jobs: if [ "${{ github.ref }}" = "refs/heads/main" ]; then # manually login to ghcr.io for publishing echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - ./scripts/publish.sh "main" + ./scripts/publish.sh --${{ matrix.os }} "main" fi + + merge: + name: 'Merge Labels (main only)' + needs: ["build"] + runs-on: ubuntu-24.04 + if: github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout (GitHub) + uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Use .devcontainer from THIS repo for building and testing + - name: Merge + uses: devcontainers/ci@v0.3 + with: + # The .devcontainer is never published as pre-built container. + # We want to only use it for building and testing the actual container, which resides in src/s-core-devcontainer. + push: "never" + runCmd: | + set -eux pipefail + + # Merge + # We do not use the push feature of devcontainers/ci here, since that would push the wrong container. + # Instead, we use the publish script which pushes the correct container (residing in src/s-core-devcontainer). + # manually login to ghcr.io for publishing + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + ./scripts/merge.sh "main" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a3970ab..4f93c6a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,11 +4,21 @@ on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' - jobs: build: - name: 'Check, Build, Test, Publish DevContainer' - runs-on: ubuntu-latest + strategy: + matrix: + os: [arm64, amd64] + include: + - os: amd64 + name: 'DevContainer (amd64)' + runner: ubuntu-24.04 + - os: arm64 + name: 'DevContainer (arm64)' + runner: ubuntu-24.04-arm + + name: '${{ matrix.name }}' + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write @@ -33,6 +43,8 @@ jobs: # We want to only use it for building and testing the actual container, which resides in src/s-core-devcontainer. push: "never" runCmd: | + set -eux pipefail + # Check pre-commit run --show-diff-on-failure --color=always --all-files || exit -1 @@ -40,7 +52,7 @@ jobs: ./scripts/create_builder.sh # Build - ./scripts/build.sh + ./scripts/build.sh --${{ matrix.os }} "${{ github.ref_name }}" "latest" # Test ./scripts/test.sh @@ -48,9 +60,43 @@ jobs: # Publish # We do not use the push feature of devcontainers/ci here, since that would push the wrong container. # Instead, we use the publish script which pushes the correct container (residing in src/s-core-devcontainer). - # manually login to ghcr.io for publishing echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + ./scripts/publish.sh --${{ matrix.os }} "${{ github.ref_name }}" "latest" + + merge: + name: 'Merge Labels' + needs: ["build"] + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout (GitHub) + uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Use .devcontainer from THIS repo for building and testing + - name: Merge + uses: devcontainers/ci@v0.3 + with: + # The .devcontainer is never published as pre-built container. + # We want to only use it for building and testing the actual container, which resides in src/s-core-devcontainer. + push: "never" + runCmd: | + set -eux pipefail - # Note: "${{ github.ref_name }}" will be the tag name, e.g., "1.0.0" - ./scripts/publish.sh "${{ github.ref_name }}" "latest" + # Merge + # We do not use the push feature of devcontainers/ci here, since that would push the wrong container. + # Instead, we use the publish script which pushes the correct container (residing in src/s-core-devcontainer). + # manually login to ghcr.io for publishing + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + ./scripts/merge.sh "${{ github.ref_name }}" "latest" diff --git a/README.md b/README.md index a1f10fe..d448c49 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,9 @@ It is very simple to develop the development container. You can change files related to the container and then simply run the `scripts/*`. They are used by the CI, but especially the build and test scripts can be run also locally out of the box: ````console -$ ./scripts/build.sh +$ ./scripts/build.sh --amd64 local [... build output..] -{"outcome":"success","imageName":["ghcr.io/eclipse-score/devcontainer"]} +{"outcome":"success","imageName":["ghcr.io/eclipse-score/devcontainer:local-amd64"]} $ ./scripts/test.sh [... test output...] @@ -133,9 +133,9 @@ So in order to execute `S-CORE DevContainer` on your host (and test it as part o Concretely, this can be done as follows: -* Run `docker save "ghcr.io/eclipse-score/devcontainer" > export.img` in `Development Container A`. +* Run `docker save "ghcr.io/eclipse-score/devcontainer:local-amd64" > export.img` in `Development Container A`. * On your **host machine** (!!), open a console and run `docker load < /path/to/export.img`. -* In the working copy of the targeted S-CORE module, edit the file `.devcontainer/devcontainer.json` and change the `"image": "..."` entry to `"image": "ghcr.io/eclipse-score/devcontainer:latest"` (if not already set like this). +* In the working copy of the targeted S-CORE module, edit the file `.devcontainer/devcontainer.json` and change the `"image": "..."` entry to `"image": "ghcr.io/eclipse-score/devcontainer:local-amd64"`. The Visual Studio Code instance related to the targeted S-CORE module will now ask you to rebuild the DevContainer. If not, press Ctrl + Shift + p and run from there "Dev Containers: Rebuilt Container Without Cache". Do so, and you have a running instance of `S-CORE DevContainer` related to the targeted S-CORE module. diff --git a/scripts/build.sh b/scripts/build.sh index dc410ff..71d4ac8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,37 +1,44 @@ #!/usr/bin/env bash set -euxo pipefail -if [ "$#" -eq 0 ]; then - echo "Error: At least one parameter (label) must be provided." +if [[ "$#" -lt 1 || "$1" != "--arm64" && "$1" != "--amd64" ]]; then + echo "Error: First parameter must be --arm64 or --amd64." exit 1 fi +if [ "$#" -lt 2 ]; then + echo "Error: At least one label must be provided after the architecture option." + exit 1 +fi + +ARCH_OPTION="$1" +shift + +ARCH="amd64" +if [[ "$ARCH_OPTION" == "--arm64" ]]; then + ARCH="arm64" +fi + LABELS=() for LABEL in "$@"; do LABELS+=("${LABEL}") done -# Define target architectures -ARCHITECTURES=("amd64" "arm64") - -# Build for each architecture, creating all requested tags -for ARCH in "${ARCHITECTURES[@]}"; do - echo "Building all labels (${LABELS[@]}) for architecture: ${ARCH}" - - # Prepare image names with tags (each tag includes a label and an architecture) - IMAGES=() - for LABEL in "${LABELS[@]}"; do - IMAGES+=("--image-name \"ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}\"") - done +echo "Building all labels (${LABELS[@]}) for architecture: ${ARCH}" - # Prepare devcontainer build command - DEVCONTAINER_CALL="devcontainer build --workspace-folder src/s-core-devcontainer --cache-from ghcr.io/eclipse-score/devcontainer" +# Prepare image names with tags (each tag includes a label and the architecture) +IMAGES=() +for LABEL in "${LABELS[@]}"; do + IMAGES+=("--image-name \"ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}\"") +done - # Append image names to the build command - for IMAGE in "${IMAGES[@]}"; do - DEVCONTAINER_CALL+=" $IMAGE" - done +# Prepare devcontainer build command +DEVCONTAINER_CALL="devcontainer build --workspace-folder src/s-core-devcontainer --cache-from ghcr.io/eclipse-score/devcontainer" - # Execute the build for the specific architecture - eval "$DEVCONTAINER_CALL --platform linux/${ARCH}" +# Append image names to the build command +for IMAGE in "${IMAGES[@]}"; do + DEVCONTAINER_CALL+=" $IMAGE" done + +# Execute the build for the specific architecture +eval "$DEVCONTAINER_CALL --platform linux/${ARCH}" diff --git a/scripts/merge.sh b/scripts/merge.sh new file mode 100755 index 0000000..600064a --- /dev/null +++ b/scripts/merge.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euxo pipefail + +if [ "$#" -eq 0 ]; then + echo "Error: At least one parameter (label) must be provided." + exit 1 +fi + +LABELS=() +for LABEL in "$@"; do + LABELS+=("${LABEL}") +done + +# Define target architectures +ARCHITECTURES=("amd64" "arm64") + +# Pull all architecture-specific images for each label +for LABEL in "${LABELS[@]}"; do + for ARCH in "${ARCHITECTURES[@]}"; do + docker pull --platform "linux/${ARCH}" "ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}" + done +done + +# Create and push the merged multiarch manifest for each tag; each tag combines all architecture-specific tags into one tag +for LABEL in "${LABELS[@]}"; do + echo "Merging all architectures (${ARCHITECTURES[@]}) into single tag: ${LABEL}" + + MANIFEST_MERGE_CALL="docker buildx imagetools create -t ghcr.io/eclipse-score/devcontainer:${LABEL}" + + for ARCH in "${ARCHITECTURES[@]}"; do + MANIFEST_MERGE_CALL+=" ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}" + done + + eval "$MANIFEST_MERGE_CALL" +done diff --git a/scripts/publish.sh b/scripts/publish.sh index 6a7586c..1fd7edb 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,50 +1,43 @@ #!/usr/bin/env bash set -euxo pipefail -if [ "$#" -eq 0 ]; then - echo "Error: At least one parameter (label) must be provided." +if [[ "$#" -lt 1 || "$1" != "--arm64" && "$1" != "--amd64" ]]; then + echo "Error: First parameter must be --arm64 or --amd64." exit 1 fi -LABELS=() -for LABEL in "$@"; do - LABELS+=("${LABEL}") -done - -# Define target architectures -ARCHITECTURES=("amd64" "arm64") - -# Build and push for each architecture, creating all requested tags -for ARCH in "${ARCHITECTURES[@]}"; do - echo "Building all tags (${LABELS[@]}) for architecture: ${ARCH}" - - # Prepare image names with tags (each tag includes a label and an architecture) - IMAGES=() - for LABEL in "${LABELS[@]}"; do - IMAGES+=("--image-name \"ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}\"") - done +if [ "$#" -lt 2 ]; then + echo "Error: At least one label must be provided after the architecture option." + exit 1 +fi - # Prepare devcontainer build command - DEVCONTAINER_CALL="devcontainer build --push --workspace-folder src/s-core-devcontainer --cache-from ghcr.io/eclipse-score/devcontainer" +ARCH_OPTION="$1" +shift - # Append image names to the build command - for IMAGE in "${IMAGES[@]}"; do - DEVCONTAINER_CALL+=" $IMAGE" - done +ARCH="amd64" +if [[ "$ARCH_OPTION" == "--arm64" ]]; then + ARCH="arm64" +fi - # Execute the build and push all tags for the specific architecture - eval "$DEVCONTAINER_CALL --platform linux/${ARCH}" +LABELS=() +for LABEL in "$@"; do + LABELS+=("${LABEL}") done -# Create and push the merged multiarch manifest for each tag; each tag combines all architecture-specific tags into one tag +echo "Building all tags (${LABELS[@]}) for architecture: ${ARCH}" +# Prepare image names with tags (each tag includes a label and an architecture) +IMAGES=() for LABEL in "${LABELS[@]}"; do - echo "Merging all architectures (${ARCHITECTURES[@]}) into single tag: ${LABEL}" - - MANIFEST_MERGE_CALL="docker buildx imagetools create -t ghcr.io/eclipse-score/devcontainer:${LABEL}" + IMAGES+=("--image-name \"ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}\"") +done - for ARCH in "${ARCHITECTURES[@]}"; do - MANIFEST_MERGE_CALL+=" ghcr.io/eclipse-score/devcontainer:${LABEL}-${ARCH}" - done +# Prepare devcontainer build command +DEVCONTAINER_CALL="devcontainer build --push --workspace-folder src/s-core-devcontainer --cache-from ghcr.io/eclipse-score/devcontainer" - eval "$MANIFEST_MERGE_CALL" +# Append image names to the build command +for IMAGE in "${IMAGES[@]}"; do + DEVCONTAINER_CALL+=" $IMAGE" done + +# Execute the build and push all tags for the specific architecture +eval "$DEVCONTAINER_CALL --platform linux/${ARCH}"