diff --git a/.github/workflows/test-slash-command.yaml b/.github/workflows/test-slash-command.yaml new file mode 100644 index 0000000..e2eb6f4 --- /dev/null +++ b/.github/workflows/test-slash-command.yaml @@ -0,0 +1,36 @@ +name: Test (Slash Command) + +# Listens for /test comments on pull requests. +# Because issue_comment always runs in the base-repo context, this workflow +# has access to repository secrets even for fork PRs. +# +# When a maintainer (write/maintain/admin) comments "/test" on a PR: +# - peter-evans/slash-command-dispatch validates the permission, adds 👀/🚀 +# reactions, and fires a repository_dispatch event of type "test-command". +# - test.yaml picks up that event and runs the matrix tests against the +# PR's head SHA (available in client_payload.pull_request). +# +# Requires a repo-scoped PAT stored as the COMMUNITY_ACTIONS_PAT secret so that the action can +# create repository_dispatch events (GITHUB_TOKEN cannot do this). + +on: + issue_comment: + types: [created] + +permissions: + contents: read + +jobs: + slash-command-dispatch: + name: Dispatch /test command + # Only process comments on pull requests + if: github.event.issue.pull_request != null + runs-on: ubuntu-latest + steps: + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@v5 + with: + token: ${{ secrets.COMMUNITY_ACTIONS_PAT }} + commands: test + permission: write + issue-type: pull-request diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bff1922..d9c617f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,49 +1,98 @@ name: Test on: - workflow_dispatch: - # Use a manual approval process before PR's are given access to - # the secrets which are required to run the integration tests. - # The PR code should be manually approved to see if it can be trusted. - # When in doubt, do not approve the test run. - # Reference: https://dev.to/petrsvihlik/using-environment-protection-rules-to-secure-secrets-when-building-external-forks-with-pullrequesttarget-hci - pull_request_target: + # Runs automatically for PRs from branches within this repo (maintainers). + # Fork PRs do not have access to secrets; maintainers must trigger those + # via a /test comment (see test-slash-command.yaml). + pull_request: branches: [ main ] + # Triggered by test-slash-command.yaml via peter-evans/slash-command-dispatch + # when a maintainer comments /test on a fork PR. + repository_dispatch: + types: [test-command] + # Allows manually triggering a test run against a specific ref from the UI. + workflow_dispatch: + inputs: + ref: + description: "Commit SHA or branch to test" + required: false + default: "" merge_group: + +permissions: + contents: read + jobs: - approve: - name: Approve - environment: - # For security reasons, all pull requests need to be approved first before granting access to secrets - # So the environment should be set to have a reviewer/s inspect it before approving it - name: ${{ github.event_name == 'pull_request_target' && 'Test Pull Request' || 'Test Auto' }} + build: + name: Build + # Skip fork PRs on the pull_request event — they have no secret access. + # Those PRs are tested via repository_dispatch triggered by test-slash-command.yaml. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - name: Wait for approval - run: echo "Approved" + - name: Checkout + uses: actions/checkout@v7 + with: + ref: ${{ github.event.client_payload.pull_request.head.sha || inputs.ref || github.sha }} + + - uses: taiki-e/install-action@just + + - uses: actions/setup-go@v6 + with: + go-version: stable + cache: false + + - run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest + name: Install dependencies + + - name: Build package (for testing) + run: just build + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: dist-packages + path: dist/ test: - name: Test - needs: [approve] - permissions: - pull-requests: write + name: Test ${{ matrix.job.name }} + needs: [build] environment: name: Test Auto + permissions: + # Required to publish system test results to the PR + issues: write + pull-requests: write + contents: read runs-on: ubuntu-latest env: - IMAGE: ${{ matrix.job.image }} + TEST_IMAGE: ${{ matrix.job.image }} strategy: fail-fast: false matrix: job: - - { image: debian-12 } + - { name: "debian-12", image: "ghcr.io/thin-edge/tedge-demo-main-systemd:latest", test_options: "" } steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + # repository_dispatch: use the fork PR's head SHA from the slash command payload + # workflow_dispatch: use the manually specified ref (or default branch) + # pull_request / merge_group: use the default (current SHA) + ref: ${{ github.event.client_payload.pull_request.head.sha || inputs.ref || github.sha }} + + - name: Download build artifacts + uses: actions/download-artifact@v8 with: - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} + name: dist-packages + path: dist/ + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 - name: create .env file run: | @@ -52,53 +101,55 @@ jobs: echo 'C8Y_USER="${{ secrets.C8Y_USER }}"' >> .env echo 'C8Y_PASSWORD="${{ secrets.C8Y_PASSWORD }}"' >> .env - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version-file: tests/.python-version cache: 'pip' cache-dependency-path: | tests/requirements.txt - uses: taiki-e/install-action@just - # - # Build - # - - uses: actions/setup-go@v5 - with: - go-version: '>=1.17.0' - cache: false - - name: Install dependencies - run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest - - name: Build package (for testing) - run: just build - # # Test # - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - name: Install dependencies run: | just venv - just build-test - name: Run tests - run: just test + run: just test ${{ matrix.job.test_options }} - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: - name: reports-${{ matrix.job.image }} + name: reports-${{ matrix.job.name }} path: output - name: Send report to commit - if: ${{ always() && github.event_name == 'pull_request_target' }} + if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'repository_dispatch') }} uses: "joonvena/robotframework-reporter-action@v2.5" with: report_path: output + show_passed_tests: "false" gh_access_token: ${{ secrets.GITHUB_TOKEN }} + pull_request_id: ${{ github.event.pull_request.number || github.event.client_payload.pull_request.number }} + sha: ${{ github.event.client_payload.pull_request.head.sha || github.sha }} + + tests-pass: + name: Tests Pass + needs: [build, test] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check all matrix jobs passed + run: | + if [ "${{ needs.build.result }}" != "success" ]; then + echo "Build job failed: ${{ needs.build.result }}" + exit 1 + fi + if [ "${{ needs.test.result }}" != "success" ]; then + echo "One or more matrix jobs failed: ${{ needs.test.result }}" + exit 1 + fi diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f941978 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +services: + tedge: + build: + dockerfile: images/Dockerfile + args: + TEST_IMAGE: ${TEST_IMAGE:-ghcr.io/thin-edge/tedge-demo-main-systemd:latest} + privileged: true diff --git a/test-images/debian-12/Dockerfile b/images/Dockerfile similarity index 82% rename from test-images/debian-12/Dockerfile rename to images/Dockerfile index 72a043c..9c8a0b9 100644 --- a/test-images/debian-12/Dockerfile +++ b/images/Dockerfile @@ -1,4 +1,5 @@ -FROM ghcr.io/thin-edge/tedge-demo-main-systemd:latest +ARG TEST_IMAGE=ghcr.io/thin-edge/tedge-demo-main-systemd:latest +FROM ${TEST_IMAGE} # FIXME: Remove once thin-edge.io 1.4.0 is released RUN wget -O - thin-edge.io/install.sh | sh -s -- --channel main @@ -12,5 +13,5 @@ RUN tedge config unset c8y.proxy.client.host \ && tedge config unset mqtt.client.host \ && tedge config unset http.client.host -COPY dist/tedge-command-plugin_*.deb /tmp/ +COPY ./dist/tedge-command-plugin_*.deb /tmp/ RUN DEBIAN_FRONTEND=noninteractive apt-get install --allow-downgrades -y --no-install-recommends /tmp/*.deb diff --git a/justfile b/justfile index ca84360..b4aa7a5 100644 --- a/justfile +++ b/justfile @@ -2,8 +2,6 @@ set positional-arguments set dotenv-load set export -IMAGE := env_var_or_default("IMAGE", "debian-12") - build *ARGS: ./ci/build.sh {{ARGS}} -- -f nfpm.tedge-command-plugin.yaml @@ -18,6 +16,3 @@ venv: # Run tests test *args='': ./.venv/bin/python3 -m robot.run --outputdir output {{args}} tests - -build-test: build - docker buildx build -t {{IMAGE}} --load -f ./test-images/{{IMAGE}}/Dockerfile . diff --git a/tests/.python-version b/tests/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/tests/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/tests/requirements.txt b/tests/requirements.txt index b634167..8f6e080 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,4 @@ robotframework~=7.0.0 -robotframework-c8y @ git+https://github.com/thin-edge/robotframework-c8y.git@0.37.2 -robotframework-devicelibrary[docker] @ git+https://github.com/thin-edge/robotframework-devicelibrary.git@1.15.1 +robotframework-c8y @ git+https://github.com/thin-edge/robotframework-c8y.git@0.51.1 +robotframework-devicelibrary[docker] @ git+https://github.com/thin-edge/robotframework-devicelibrary.git@1.26.1 robotframework-debuglibrary~=2.5.0 diff --git a/tests/resources/common.robot b/tests/resources/common.robot index 116d410..33531cd 100644 --- a/tests/resources/common.robot +++ b/tests/resources/common.robot @@ -1,11 +1,41 @@ *** Settings *** Library Cumulocity +Library DeviceLibrary *** Variables *** # Cumulocity settings &{C8Y_CONFIG} host=%{C8Y_BASEURL= } username=%{C8Y_USER= } password=%{C8Y_PASSWORD= } tenant=%{C8Y_TENANT= } -# Docker adapter settings (to control which image is used in the system tests). -# The user just needs to set the IMAGE env variable -&{DOCKER_CONFIG} image=%{IMAGE= } +*** Keywords *** + +Setup Device + ${DEVICE_ID}= DeviceLibrary.Setup compose_file=${CURDIR}/../../docker-compose.yaml skip_bootstrap=${True} + Set Test Variable $DEVICE_ID + Bootstrap Device ${DEVICE_ID} + +Bootstrap Device + [Arguments] ${device_id} + ${domain}= Cumulocity.Get Domain + DeviceLibrary.Execute Command cmd=tedge config set c8y.url ${domain} + ${credentials}= Cumulocity.Bulk Register Device With Cumulocity CA external_id=${device_id} + DeviceLibrary.Execute Command cmd=tedge cert download c8y --device-id ${device_id} --retry-every 5s --one-time-password '${credentials.one_time_password}' + DeviceLibrary.Execute Command cmd=tedge reconnect c8y + Cumulocity.External Identity Should Exist external_id=${device_id} + ${operation}= Cumulocity.Get Configuration typename=tedge-configuration-plugin timeout=60 + Cumulocity.Operation Should Be SUCCESSFUL ${operation} + Sleep 2s + +Collect Logs + Run Keyword And Continue On Failure Get Workflow Logs + Run Keyword And Continue On Failure Get Service Logs + +Get Workflow Logs + DeviceLibrary.Execute Command head -n-0 /var/log/tedge/agent/* + +Get Service Logs + DeviceLibrary.Execute Command cmd=journalctl -n 10000 -u "tedge-*" -u "c8y-*" --no-pager + +Teardown Device + Collect Logs + Cumulocity.Delete Managed Object And Device User external_id=${DEVICE_ID} diff --git a/tests/shell.robot b/tests/shell.robot index 9cbd52c..d2f9549 100644 --- a/tests/shell.robot +++ b/tests/shell.robot @@ -1,10 +1,8 @@ *** Settings *** Resource ./resources/common.robot -Library Cumulocity -Library DeviceLibrary bootstrap_script=bootstrap.sh -Suite Setup Test Setup -Suite Teardown Collect Logs +Test Setup Setup Device +Test Teardown Teardown Device *** Test Cases *** @@ -27,19 +25,10 @@ Execute command that fails Operation Should Be FAILED ${operation} -*** Keywords *** - -Test Setup - ${DEVICE_SN}= Setup - Set Suite Variable $DEVICE_SN - Device Should Exist ${DEVICE_SN} - -Collect Logs - Get Workflow Logs - Get Service Logs - -Get Workflow Logs - DeviceLibrary.Execute Command head -n-0 /var/log/tedge/agent/* +Execute command works even if the /tmp folder is full + Execute Command cmd=dd if=/dev/zero of=/tmp/example bs=$((64)) count=$((10000*1024)) || true + ${operation}= Cumulocity.Create Operation + ... description=execute command and ignore output + ... fragments={"c8y_Command":{"text":"echo helloworld > /dev/null"}} -Get Service Logs - DeviceLibrary.Execute Command journalctl --no-pager + Operation Should Be SUCCESSFUL ${operation}