From 3019063ac7ac4b324e6c4a0f35517735e26ded91 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Thu, 19 Mar 2026 17:39:44 +0100 Subject: [PATCH 1/6] Updated educates CLI to latest and removed dependency on carvel. Added support for include/excelude in multi-workshop --- publish-multiple-workshops/README.md | 35 ++++++++ publish-multiple-workshops/action.yaml | 112 +++++++++++++++++++------ publish-workshop/action.yaml | 35 ++++---- 3 files changed, 135 insertions(+), 47 deletions(-) diff --git a/publish-multiple-workshops/README.md b/publish-multiple-workshops/README.md index 720015b..14087c9 100644 --- a/publish-multiple-workshops/README.md +++ b/publish-multiple-workshops/README.md @@ -140,5 +140,40 @@ GitHub action are as follows: |---------------------------------|----------|----------|------------------------------------| | `path` | False | String | Relative directory path under `$GITHUB_WORKSPACE` to the collection of workshops. Defaults to "`workshops`". | | `token` | True | String | GitHub access token. Must be set to `${{secrets.GITHUB_TOKEN}}` or appropriate personal access token variable reference. | +| `include` | False | String | Optional list of workshop directory names/patterns to include (one per line, or comma/space separated; supports glob patterns like "`lab-*`"). If empty, all workshops are included. | +| `exclude` | False | String | Optional list of workshop directory names/patterns to exclude (one per line, or comma/space separated; supports glob patterns like "`lab-*`"). Applied after include. | | `trainingportal-resource-file` | False | String | Relative path under `$GITHUB_WORKSPACE` to the `TrainingPortal` resource file. Defaults to "`resources/trainingportal.yaml`". | | `workshop-resource-file` | False | String | Relative path under workshop directory to the `Workshop` resource file. Defaults to "`resources/workshop.yaml`". Every workshop must have same directory structure. | + +Filtering workshops +------------------- + +Use `include` and/or `exclude` to filter which workshop directories under `path` are published. + +Examples: + +```yaml +- name: Create release (only publish lab-* workshops) + uses: educates/educates-github-actions/publish-multiple-workshops@v7 + with: + token: ${{secrets.GITHUB_TOKEN}} + include: lab-* +``` + +```yaml +- name: Create release (publish all except some, comma separated) + uses: educates/educates-github-actions/publish-multiple-workshops@v7 + with: + token: ${{secrets.GITHUB_TOKEN}} + exclude: "lab-examiner-scripts, lab-docker-runtime" +``` + +```yaml +- name: Create release (publish all except some, multiline) + uses: educates/educates-github-actions/publish-multiple-workshops@v7 + with: + token: ${{secrets.GITHUB_TOKEN}} + exclude: | + lab-examiner-scripts + lab-docker-runtime +``` diff --git a/publish-multiple-workshops/action.yaml b/publish-multiple-workshops/action.yaml index ee89920..492499c 100644 --- a/publish-multiple-workshops/action.yaml +++ b/publish-multiple-workshops/action.yaml @@ -9,6 +9,14 @@ inputs: description: "Relative directory path under $GITHUB_WORKSPACE to the collection of workshops." required: false default: 'workshops' + include: + description: "Optional list of workshop directory names/patterns to include (one per line, or comma/space separated; supports glob patterns like 'lab-*'). If empty, all workshops are included." + required: false + default: '' + exclude: + description: "Optional list of workshop directory names/patterns to exclude (one per line, or comma/space separated; supports glob patterns like 'lab-*'). Applied after include." + required: false + default: '' workshop-resource-file: description: "Relative path under workshop directory to the workshops resource file. All workshops must have same directory structure." required: false @@ -26,16 +34,6 @@ runs: using: composite steps: - - name: Install Carvel tools - shell: bash - run: curl -L https://carvel.dev/install.sh | bash - - - name: Install Educates CLI - shell: bash - run: | - imgpkg pull -i ghcr.io/educates/educates-client-programs:3.3.2 -o /tmp/client-programs - mv /tmp/client-programs/educates-linux-amd64 /usr/local/bin/educates - - name: Calculate release variables shell: bash run: | @@ -47,44 +45,107 @@ runs: - name: Publish workshop content as OCI image and create workshop definition shell: bash - run : | + run: | + matches_any_pattern() { + local value="$1" + local patterns="$2" + + [[ -z "${patterns}" ]] && return 1 + + patterns="${patterns//,/ }" + patterns="${patterns//$'\n'/ }" + for pattern in ${patterns}; do + [[ -z "${pattern}" ]] && continue + case "${value}" in + ${pattern}) return 0 ;; + esac + done + + return 1 + } + + WORKSHOPS_ROOT="${{inputs.path}}" + mkdir -p ${{runner.temp}}/release - for WORKSHOP_NAME in `ls ${{inputs.path}}`; do + + published=0 + + shopt -s nullglob + for WORKSHOP_DIR in "${WORKSHOPS_ROOT}"/*; do + [[ -d "${WORKSHOP_DIR}" ]] || continue + + WORKSHOP_NAME="$(basename "${WORKSHOP_DIR}")" + + if [[ -n "${{inputs.include}}" ]] && ! matches_any_pattern "${WORKSHOP_NAME}" "${{inputs.include}}"; then + echo "Skipping workshop (not included): ${WORKSHOP_NAME}" + continue + fi + + if matches_any_pattern "${WORKSHOP_NAME}" "${{inputs.exclude}}"; then + echo "Skipping workshop (excluded): ${WORKSHOP_NAME}" + continue + fi + mkdir -p ${{runner.temp}}/workshops/${WORKSHOP_NAME}/resources echo "Publishing workshop: ${WORKSHOP_NAME}" - educates publish-workshop ${{inputs.path}}/${WORKSHOP_NAME} \ - --export-workshop ${{runner.temp}}/workshops/${WORKSHOP_NAME}/resources/workshop.yaml \ - --image-repository=ghcr.io/${{env.REPOSITORY_OWNER}} \ - --workshop-version=${{env.REPOSITORY_TAG}} \ - --registry-username=${{github.actor}} \ - --registry-password=${{env.GITHUB_TOKEN}} + docker run --rm \ + -v ${{github.workspace}}:/workspace \ + -v ${{runner.temp}}:/tmp/output \ + ghcr.io/educates/educates-cli:3.6.1 publish-workshop /workspace/${WORKSHOP_DIR} \ + --workshop-file '${{inputs.workshop-resource-file}}' \ + --export-workshop /tmp/output/workshops/${WORKSHOP_NAME}/resources/workshop.yaml \ + --image-repository=ghcr.io/${{env.REPOSITORY_OWNER}} \ + --workshop-version=${{env.REPOSITORY_TAG}} \ + --registry-username=${{github.actor}} \ + --registry-password=${{env.GITHUB_TOKEN}} echo "Make a releasable copy of the workshop definition" cp ${{runner.temp}}/workshops/${WORKSHOP_NAME}/resources/workshop.yaml ${{runner.temp}}/release/${WORKSHOP_NAME}.yaml + + published=$((published + 1)) done + if [[ ${published} -eq 0 ]]; then + echo "No workshops matched include/exclude filters under '${WORKSHOPS_ROOT}'." + exit 1 + fi + + echo "Published ${published} workshop(s)." + + # If the trainingportal resource file is available, copy it to the release directory + # else, this will fail the job + cp "${{inputs.trainingportal-resource-file}}" ${{runner.temp}}/release/trainingportal.yaml + - name: Generate archives containing the workshop definition shell: bash run: | - ytt -f ${{runner.temp}}/workshops > ${{runner.temp}}/workshops.yaml + first=true + for f in ${{runner.temp}}/workshops/*/resources/workshop.yaml; do + if [ "$first" = true ]; then + first=false + else + echo "---" + fi + cat "$f" + done > ${{runner.temp}}/workshops.yaml (cd ${{runner.temp}}; tar cvfz workshops.tar.gz workshops) (cd ${{runner.temp}}; zip workshops.zip -r workshops) - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: individual-workshops path: ${{runner.temp}}/release/*.yaml - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: workshops.yaml path: ${{runner.temp}}/workshops.yaml - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: workshops.tar.gz path: ${{runner.temp}}/workshops.tar.gz - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: workshops.zip path: ${{runner.temp}}/workshops.zip @@ -100,10 +161,9 @@ runs: name: "${{env.REPOSITORY_NAME}}:${{env.REPOSITORY_TAG}}" draft: false prerelease: false - fail_on_unmatched_files: false + fail_on_unmatched_files: true files: | ${{runner.temp}}/workshops.tar.gz ${{runner.temp}}/workshops.zip ${{runner.temp}}/workshops.yaml - ${{inputs.path}}/${{inputs.trainingportal-resource-file}} - ${{runner.temp}}/release/*.yaml \ No newline at end of file + ${{runner.temp}}/release/*.yaml diff --git a/publish-workshop/action.yaml b/publish-workshop/action.yaml index e3e7460..fca76b2 100644 --- a/publish-workshop/action.yaml +++ b/publish-workshop/action.yaml @@ -22,16 +22,6 @@ runs: using: composite steps: - - name: Install Carvel tools - shell: bash - run: curl -L https://carvel.dev/install.sh | bash - - - name: Install Educates CLI - shell: bash - run: | - imgpkg pull -i ghcr.io/educates/educates-client-programs:3.3.2 -o /tmp/client-programs - mv /tmp/client-programs/educates-linux-amd64 /usr/local/bin/educates - - name: Calculate release variables shell: bash run: | @@ -43,15 +33,18 @@ runs: - name: Publish workshop and create workshop definition shell: bash - run : | + run: | mkdir -p ${{runner.temp}}/workshops/${REPOSITORY_NAME}/resources - educates publish-workshop '${{inputs.path}}' \ - --workshop-file '${{inputs.workshop-resource-file}}' \ - --export-workshop ${{runner.temp}}/workshops/${REPOSITORY_NAME}/resources/workshop.yaml \ - --image-repository=ghcr.io/${REPOSITORY_OWNER} \ - --workshop-version=${REPOSITORY_TAG} \ - --registry-username=${{github.actor}} \ - --registry-password=${{env.GITHUB_TOKEN}} + docker run --rm \ + -v ${{github.workspace}}:/workspace \ + -v ${{runner.temp}}:/tmp/output \ + ghcr.io/educates/educates-cli:3.6.1 publish-workshop /workspace/${{inputs.path}} \ + --workshop-file '${{inputs.workshop-resource-file}}' \ + --export-workshop /tmp/output/workshops/${REPOSITORY_NAME}/resources/workshop.yaml \ + --image-repository=ghcr.io/${REPOSITORY_OWNER} \ + --workshop-version=${REPOSITORY_TAG} \ + --registry-username=${{github.actor}} \ + --registry-password=${{env.GITHUB_TOKEN}} - name: Generate archives containing the workshop definition shell: bash @@ -59,17 +52,17 @@ runs: (cd ${{runner.temp}}; tar cvfz workshops.tar.gz workshops) (cd ${{runner.temp}}; zip workshops.zip -r workshops) - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: workshop.yaml path: ${{runner.temp}}/workshops/${{env.REPOSITORY_NAME}}/resources/workshop.yaml - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: workshops.tar.gz path: ${{runner.temp}}/workshops.tar.gz - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: workshops.zip path: ${{runner.temp}}/workshops.zip From 2ca993afb5752a63e33ec3de3425da74e82e0d92 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Thu, 19 Mar 2026 18:09:22 +0100 Subject: [PATCH 2/6] Adding access to runner host CA --- publish-multiple-workshops/action.yaml | 1 + publish-workshop/action.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/publish-multiple-workshops/action.yaml b/publish-multiple-workshops/action.yaml index 492499c..74d554a 100644 --- a/publish-multiple-workshops/action.yaml +++ b/publish-multiple-workshops/action.yaml @@ -91,6 +91,7 @@ runs: docker run --rm \ -v ${{github.workspace}}:/workspace \ -v ${{runner.temp}}:/tmp/output \ + -v /etc/ssl/certs:/etc/ssl/certs:ro \ ghcr.io/educates/educates-cli:3.6.1 publish-workshop /workspace/${WORKSHOP_DIR} \ --workshop-file '${{inputs.workshop-resource-file}}' \ --export-workshop /tmp/output/workshops/${WORKSHOP_NAME}/resources/workshop.yaml \ diff --git a/publish-workshop/action.yaml b/publish-workshop/action.yaml index fca76b2..6bb80fa 100644 --- a/publish-workshop/action.yaml +++ b/publish-workshop/action.yaml @@ -38,6 +38,7 @@ runs: docker run --rm \ -v ${{github.workspace}}:/workspace \ -v ${{runner.temp}}:/tmp/output \ + -v /etc/ssl/certs:/etc/ssl/certs:ro \ ghcr.io/educates/educates-cli:3.6.1 publish-workshop /workspace/${{inputs.path}} \ --workshop-file '${{inputs.workshop-resource-file}}' \ --export-workshop /tmp/output/workshops/${REPOSITORY_NAME}/resources/workshop.yaml \ From d015e37b464e74d610b9b03ea6e24d0565a4fd57 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Thu, 19 Mar 2026 18:14:34 +0100 Subject: [PATCH 3/6] Fixing glob expansion --- publish-multiple-workshops/action.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/publish-multiple-workshops/action.yaml b/publish-multiple-workshops/action.yaml index 74d554a..62feb72 100644 --- a/publish-multiple-workshops/action.yaml +++ b/publish-multiple-workshops/action.yaml @@ -54,12 +54,14 @@ runs: patterns="${patterns//,/ }" patterns="${patterns//$'\n'/ }" + set -f # disable pathname expansion to preserve glob patterns for pattern in ${patterns}; do [[ -z "${pattern}" ]] && continue case "${value}" in ${pattern}) return 0 ;; esac done + set +f return 1 } From ba40640b7beb5b277a6c1e07a4a61824053dde6c Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Thu, 19 Mar 2026 18:22:45 +0100 Subject: [PATCH 4/6] Not fail on missing trainingportal, but warn --- publish-multiple-workshops/action.yaml | 10 ++++++---- publish-workshop/action.yaml | 9 +++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/publish-multiple-workshops/action.yaml b/publish-multiple-workshops/action.yaml index 62feb72..b29c76b 100644 --- a/publish-multiple-workshops/action.yaml +++ b/publish-multiple-workshops/action.yaml @@ -114,9 +114,11 @@ runs: echo "Published ${published} workshop(s)." - # If the trainingportal resource file is available, copy it to the release directory - # else, this will fail the job - cp "${{inputs.trainingportal-resource-file}}" ${{runner.temp}}/release/trainingportal.yaml + if [[ -f "${{inputs.trainingportal-resource-file}}" ]]; then + cp "${{inputs.trainingportal-resource-file}}" ${{runner.temp}}/release/trainingportal.yaml + else + echo "::warning::TrainingPortal resource file '${{inputs.trainingportal-resource-file}}' not found. Skipping." + fi - name: Generate archives containing the workshop definition shell: bash @@ -164,7 +166,7 @@ runs: name: "${{env.REPOSITORY_NAME}}:${{env.REPOSITORY_TAG}}" draft: false prerelease: false - fail_on_unmatched_files: true + fail_on_unmatched_files: false files: | ${{runner.temp}}/workshops.tar.gz ${{runner.temp}}/workshops.zip diff --git a/publish-workshop/action.yaml b/publish-workshop/action.yaml index 6bb80fa..4336ca1 100644 --- a/publish-workshop/action.yaml +++ b/publish-workshop/action.yaml @@ -47,6 +47,12 @@ runs: --registry-username=${{github.actor}} \ --registry-password=${{env.GITHUB_TOKEN}} + if [[ -f "${{inputs.path}}/${{inputs.trainingportal-resource-file}}" ]]; then + cp "${{inputs.path}}/${{inputs.trainingportal-resource-file}}" ${{runner.temp}}/workshops/${REPOSITORY_NAME}/resources/trainingportal.yaml + else + echo "::warning::TrainingPortal resource file '${{inputs.path}}/${{inputs.trainingportal-resource-file}}' not found. Skipping." + fi + - name: Generate archives containing the workshop definition shell: bash run: | @@ -83,5 +89,4 @@ runs: files: | ${{runner.temp}}/workshops.tar.gz ${{runner.temp}}/workshops.zip - ${{runner.temp}}/workshops/${{env.REPOSITORY_NAME}}/resources/workshop.yaml - ${{inputs.path}}/${{inputs.trainingportal-resource-file}} + ${{runner.temp}}/workshops/${{env.REPOSITORY_NAME}}/resources/*.yaml From ffabf8935492e8db9486c9f4491c3b03bad3b024 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Thu, 19 Mar 2026 18:37:52 +0100 Subject: [PATCH 5/6] Include takes precendence over exclude --- publish-multiple-workshops/README.md | 15 ++++++++++++++- publish-multiple-workshops/action.yaml | 8 ++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/publish-multiple-workshops/README.md b/publish-multiple-workshops/README.md index 14087c9..23a7f1e 100644 --- a/publish-multiple-workshops/README.md +++ b/publish-multiple-workshops/README.md @@ -141,7 +141,7 @@ GitHub action are as follows: | `path` | False | String | Relative directory path under `$GITHUB_WORKSPACE` to the collection of workshops. Defaults to "`workshops`". | | `token` | True | String | GitHub access token. Must be set to `${{secrets.GITHUB_TOKEN}}` or appropriate personal access token variable reference. | | `include` | False | String | Optional list of workshop directory names/patterns to include (one per line, or comma/space separated; supports glob patterns like "`lab-*`"). If empty, all workshops are included. | -| `exclude` | False | String | Optional list of workshop directory names/patterns to exclude (one per line, or comma/space separated; supports glob patterns like "`lab-*`"). Applied after include. | +| `exclude` | False | String | Optional list of workshop directory names/patterns to exclude (one per line, or comma/space separated; supports glob patterns like "`lab-*`"). `include` takes precedence over `exclude` on a per-workshop basis. | | `trainingportal-resource-file` | False | String | Relative path under `$GITHUB_WORKSPACE` to the `TrainingPortal` resource file. Defaults to "`resources/trainingportal.yaml`". | | `workshop-resource-file` | False | String | Relative path under workshop directory to the `Workshop` resource file. Defaults to "`resources/workshop.yaml`". Every workshop must have same directory structure. | @@ -150,6 +150,8 @@ Filtering workshops Use `include` and/or `exclude` to filter which workshop directories under `path` are published. +When both `include` and `exclude` are specified, `include` takes precedence on a per-workshop basis: any workshop that matches an `include` pattern is published regardless of `exclude` rules. The `exclude` rules only apply to workshops that are not explicitly included. + Examples: ```yaml @@ -177,3 +179,14 @@ Examples: lab-examiner-scripts lab-docker-runtime ``` + +```yaml +- name: Create release (include takes precedence over exclude) + uses: educates/educates-github-actions/publish-multiple-workshops@v7 + with: + token: ${{secrets.GITHUB_TOKEN}} + include: | + lab-builtin-vcluster + lab-command-* + exclude: lab-* +``` diff --git a/publish-multiple-workshops/action.yaml b/publish-multiple-workshops/action.yaml index b29c76b..b7d1e8b 100644 --- a/publish-multiple-workshops/action.yaml +++ b/publish-multiple-workshops/action.yaml @@ -78,12 +78,12 @@ runs: WORKSHOP_NAME="$(basename "${WORKSHOP_DIR}")" - if [[ -n "${{inputs.include}}" ]] && ! matches_any_pattern "${WORKSHOP_NAME}" "${{inputs.include}}"; then + if [[ -n "${{inputs.include}}" ]] && matches_any_pattern "${WORKSHOP_NAME}" "${{inputs.include}}"; then + echo "Workshop explicitly included: ${WORKSHOP_NAME}" + elif [[ -n "${{inputs.include}}" ]]; then echo "Skipping workshop (not included): ${WORKSHOP_NAME}" continue - fi - - if matches_any_pattern "${WORKSHOP_NAME}" "${{inputs.exclude}}"; then + elif matches_any_pattern "${WORKSHOP_NAME}" "${{inputs.exclude}}"; then echo "Skipping workshop (excluded): ${WORKSHOP_NAME}" continue fi From bae1e4b2093398bdaddd4ad9fd05dc9ad88c95b2 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Wed, 25 Mar 2026 20:25:58 +0100 Subject: [PATCH 6/6] Adding a note on required permissions for users --- README.md | 11 +++++++++++ publish-multiple-workshops/README.md | 11 +++++++++++ publish-workshop/README.md | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/README.md b/README.md index 9e1f023..f668a28 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,14 @@ The GitHub actions included here are: Note that versioning applies to the collection as a whole. This means that if a breaking change is made to a single action, then the version is incremented on all actions, even though changes may not have been made to the other actions. + +Permissions +----------- + +These actions requires the following `GITHUB_TOKEN` permissions: + +```yaml +permissions: + contents: write # To create/update GitHub releases + packages: write # To push OCI image artifacts to GHCR +``` diff --git a/publish-multiple-workshops/README.md b/publish-multiple-workshops/README.md index 23a7f1e..9c55346 100644 --- a/publish-multiple-workshops/README.md +++ b/publish-multiple-workshops/README.md @@ -190,3 +190,14 @@ Examples: lab-command-* exclude: lab-* ``` + +Permissions +----------- + +This action requires the following `GITHUB_TOKEN` permissions: + +```yaml +permissions: + contents: write # To create/update GitHub releases + packages: write # To push OCI image artifacts to GHCR +``` diff --git a/publish-workshop/README.md b/publish-workshop/README.md index 350d5e5..266ce8b 100644 --- a/publish-workshop/README.md +++ b/publish-workshop/README.md @@ -105,3 +105,14 @@ GitHub action are as follows: | `token` | True | String | GitHub access token. Must be set to `${{secrets.GITHUB_TOKEN}}` or appropriate personal access token variable reference. | | `trainingportal-resource-file` | False | String | Relative path under workshop directory to the `TrainingPortal` resource file. Defaults to "`resources/trainingportal.yaml`". | | `workshop-resource-file` | False | String | Relative path under workshop directory to the `Workshop` resource file. Defaults to "`resources/workshop.yaml`". | + +Permissions +----------- + +This action requires the following `GITHUB_TOKEN` permissions: + +```yaml +permissions: + contents: write # To create/update GitHub releases + packages: write # To push OCI image artifacts to GHCR +```