diff --git a/.github/workflows/demo-command.yaml b/.github/workflows/demo-command.yaml new file mode 100644 index 0000000..929f87e --- /dev/null +++ b/.github/workflows/demo-command.yaml @@ -0,0 +1,111 @@ +name: Demo command + +# Minimal example of a PR comment command built on comment-triggered-check. +# Copy this file and demo-suite.yaml to add a command to another repository. +# +# /demo-e2e ok the check passes +# /demo-e2e fail the check fails +# /demo-e2e nothing the check ends as "Nothing to run" +# +# issue_comment runs this file from the default branch, with secrets, for every +# comment. So this workflow never checks out PR code. It only parses the +# command, opens a check on the PR's head commit and starts demo-suite.yaml. + +on: + issue_comment: + types: [created] + +permissions: {} + +jobs: + prepare: + name: Parse command + # Cheap pre-filter; the action does the real parsing. + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/demo-e2e') }} + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + outputs: + should-run: ${{ steps.command.outputs.should-run }} + filter: ${{ steps.command.outputs.filter }} + is-fork: ${{ steps.command.outputs.is-fork }} + head-ref: ${{ steps.command.outputs.head-ref }} + base-ref: ${{ steps.command.outputs.base-ref }} + head-sha: ${{ steps.command.outputs.head-sha }} + key: ${{ steps.command.outputs.concurrency-key }} + check-run-id: ${{ steps.command.outputs.check-run-id }} + steps: + # Parses the comment, checks the author, finds the head commit and opens + # the check on it. + - name: Parse command and open check + id: command + uses: loft-sh/github-actions/.github/actions/comment-triggered-check@comment-triggered-check/v1 + with: + mode: start + command: /demo-e2e + check-name-prefix: demo-e2e + finish-workflow: demo-suite.yaml + comment-body: ${{ github.event.comment.body }} + comment-author: ${{ github.event.comment.user.login }} + author-association: ${{ github.event.comment.author_association }} + pr-number: ${{ github.event.issue.number }} + run-id: ${{ github.run_id }} + server-url: ${{ github.server_url }} + # Fork PRs: safe only because demo-suite.yaml checks out head-sha + # and only reads the cache. + # allow-forks: true + # Give the check its own group on the PR (also pass to finish). + # app-client-id: ${{ vars.CI_APP_CLIENT_ID }} + # app-private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} + + # A typo or a missing permission gets a reply instead of a run. + - name: Explain refusal + if: ${{ steps.command.outputs.matched == 'true' && steps.command.outputs.should-run != 'true' && steps.command.outputs.reason-title != '' }} + uses: loft-sh/github-actions/.github/actions/sticky-pr-comment@sticky-pr-comment/v1 + with: + pr-number: ${{ github.event.issue.number }} + marker: '' + body: | + **${{ steps.command.outputs.reason-title }}** + + ${{ steps.command.outputs.reason-guidance }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + dispatch: + name: Start suite + needs: [prepare] + if: ${{ needs.prepare.outputs.should-run == 'true' }} + runs-on: ubuntu-latest + permissions: + actions: write + checks: write + steps: + # Same-repo PRs run their own branch's suite. Forks run the target + # branch's suite, because a fork's workflow files are not trusted. + - name: Dispatch demo-suite.yaml + id: dispatch + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + WORKFLOW_REF: ${{ needs.prepare.outputs.is-fork == 'true' && needs.prepare.outputs.base-ref || needs.prepare.outputs.head-ref }} + FILTER: ${{ needs.prepare.outputs.filter }} + HEAD_SHA: ${{ needs.prepare.outputs.head-sha }} + KEY: ${{ needs.prepare.outputs.key }} + CHECK_RUN_ID: ${{ needs.prepare.outputs.check-run-id }} + run: | + gh workflow run demo-suite.yaml --ref "$WORKFLOW_REF" \ + -f "filter=$FILTER" -f "head-sha=$HEAD_SHA" \ + -f "concurrency-key=$KEY" -f "check-run-id=$CHECK_RUN_ID" + + # Never leave a check spinning: if the suite could not start, close it. + - name: Close the check if the suite did not start + if: ${{ steps.dispatch.outcome == 'failure' }} + uses: loft-sh/github-actions/.github/actions/comment-triggered-check@comment-triggered-check/v1 + with: + mode: finish + check-run-id: ${{ needs.prepare.outputs.check-run-id }} + suite-result: failure + summary: Could not start demo-suite.yaml. Update the branch from its target and comment again. + details-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/demo-suite.yaml b/.github/workflows/demo-suite.yaml new file mode 100644 index 0000000..eccc19e --- /dev/null +++ b/.github/workflows/demo-suite.yaml @@ -0,0 +1,94 @@ +name: Demo suite + +# The non-privileged half of demo-command.yaml. It runs the requested tests +# against the exact commit the check was opened on, then closes the check. +run-name: "demo-e2e: ${{ inputs.filter }}" + +on: + workflow_dispatch: + inputs: + filter: + description: Filter from the comment. + required: true + type: string + head-sha: + description: Commit the check was opened on. + required: true + type: string + concurrency-key: + description: Request identity from the action. + required: true + type: string + check-run-id: + description: Check to close when the suite ends. + required: true + type: string + +permissions: {} + +# A fork run can restore caches but never save them. +cache-mode: read + +jobs: + suite: + name: Run suite + runs-on: ubuntu-latest + permissions: + contents: read + # The same request again on the same PR replaces the older run. + concurrency: + group: demo-suite-${{ inputs.concurrency-key }} + cancel-in-progress: true + outputs: + check-conclusion: ${{ steps.result.outputs.check-conclusion }} + steps: + # The workflow comes from a trusted branch; the code comes from head-sha. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.head-sha }} + persist-credentials: false + + - name: Verify the requested commit + env: + EXPECTED_HEAD_SHA: ${{ inputs.head-sha }} + run: | + ACTUAL_HEAD_SHA="$(git rev-parse HEAD)" + if [[ "$EXPECTED_HEAD_SHA" != "$ACTUAL_HEAD_SHA" ]]; then + echo "::error::expected ${EXPECTED_HEAD_SHA}, checked out ${ACTUAL_HEAD_SHA}. Comment again." + exit 1 + fi + echo "testing ${ACTUAL_HEAD_SHA}" + + # Stand-in for the real tests (Ginkgo, go test, ...). It must report one + # of success, failure, neutral or timed_out. + - name: Run tests + id: result + env: + FILTER: ${{ inputs.filter }} + run: | + echo "running tests matching: ${FILTER}" + sleep 20 + case "$FILTER" in + *fail*) echo "check-conclusion=failure" >> "$GITHUB_OUTPUT" ;; + *nothing*) echo "check-conclusion=neutral" >> "$GITHUB_OUTPUT" ;; + *) echo "check-conclusion=success" >> "$GITHUB_OUTPUT" ;; + esac + + # Runs even when the suite fails or is cancelled, so the check always closes. + finish: + name: Close check + needs: [suite] + if: ${{ always() && inputs.check-run-id != '' }} + runs-on: ubuntu-latest + permissions: + checks: write + steps: + - uses: loft-sh/github-actions/.github/actions/comment-triggered-check@comment-triggered-check/v1 + with: + mode: finish + check-run-id: ${{ inputs.check-run-id }} + report-conclusion: ${{ needs.suite.result != 'cancelled' && needs.suite.outputs.check-conclusion || '' }} + suite-result: ${{ needs.suite.result }} + details-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + # app-client-id: ${{ vars.CI_APP_CLIENT_ID }} + # app-private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} diff --git a/README.md b/README.md index 18b06e3..080198a 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,19 @@ This repo contains caller workflows that exercise each reusable workflow from | `caller-secret-broker-request.yaml` | creates and queues a short-lived broker request | | `caller-secret-broker-response.yaml` | `secret-broker-request` action | +## Demo command + +`demo-command.yaml` and `demo-suite.yaml` are a minimal, commented example of a +PR comment command built on `comment-triggered-check`. Copy both to add a +command to another repository. Comment on any PR here: + +| Comment | Check result | +|---------|--------------| +| `/demo-e2e ok` | passes | +| `/demo-e2e fail` | fails | +| `/demo-e2e nothing` | "Nothing to run" | +| `/demo-e2e (ok` | no check; a reply explains the unbalanced parenthesis | + ## Integration test orchestrator `integration-tests.yaml` runs all E2E tests in parallel, creating real PRs,