**Related issue:** Resolves #48007 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## What When Dependabot opens a code-dependency PR (Go or npm), a GitHub Actions workflow now auto-creates a QA tracking issue assigned to @AndreyKizimenko and places it on the `:help-qa` board in "Ready" status. Also exempts `dependabot[bot]` from the PR template check. ### Files changed - **`.github/workflows/dependabot-qa-ticket.yml`** (new) - The core workflow. Triggers on `pull_request_target[opened]`, filters for code-dependency Dependabot PRs (Go modules + npm, excluding website/slackbot), creates a QA tracking issue, comments on the PR, and adds the issue to the `:help-qa` project board. - **`.github/workflows/check-pr-template.yml`** - Exempts `dependabot[bot]` from the PR description template check (Dependabot PRs have auto-generated descriptions). - **`.github/zizmor-gate.yml`** - Suppresses two expected zizmor findings: `dangerous-triggers` (uses `pull_request_target` but never checks out PR code) and `bot-conditions` (gates on `user.login` which is not spoofable for same-repo Dependabot PRs). - **`changes/48007-dependabot-qa-ticket`** - Release note. ## Security review This workflow uses `pull_request_target`, a known attack surface for GitHub Actions. A dedicated security audit was performed covering all standard threat vectors: | Area | Status | Details | |------|--------|---------| | **Code checkout injection** | SAFE | No `actions/checkout` or `git clone`. The workflow only calls the GitHub API via `actions/github-script`. The inline JavaScript is embedded in the base-branch YAML, not loaded from PR code. | | **Script/template injection** | SAFE | PR data (`pr.title`, `pr.head.ref`) is used in JavaScript template literals passed to the GitHub API as strings. There is no shell interpolation, no `eval()`, no `${{ }}` expressions in `run:` blocks. | | **Bot identity spoofing** | SAFE | Gates on `github.event.pull_request.user.login == 'dependabot[bot]'`, which is server-set by GitHub and cannot be spoofed. Uses `user.login` (immutable) rather than `github.actor` (which can differ on re-runs). The `[bot]` suffix is reserved for GitHub App accounts. | | **Secret exposure** | SAFE | `FLEET_GITHUB_TOKEN_PROJECTS` is passed via `env:` block, accessed as `process.env.PROJECT_TOKEN`, and used only as an argument to `getOctokit()`. Never logged, never interpolated into output. | | **Permission scope** | MINIMAL | Top-level: `contents: read`. Job-level: `contents: read` + `issues: write`. No `pull-requests: write` (PR comments use the Issues API). | | **Idempotency** | SAFE | Before creating an issue, searches for an existing open issue with the same title and `dependabot` label. Prevents duplicates on workflow re-runs. | | **Markdown injection** | LOW (theoretical) | `pr.title` is interpolated into issue title/body. Dependabot controls the title format ("Bump X from Y to Z") and package names are registry-constrained. Not exploitable without compromising Dependabot itself. | | **Denial of service** | LOW (theoretical) | Bounded by Dependabot's own rate limits (default 5 PRs/ecosystem/day) plus the idempotency guard. | | **Zizmor waivers** | JUSTIFIED | `dangerous-triggers`: no code checkout, API-only. `bot-conditions`: uses `user.login` not `github.actor`. Both include inline rationale comments. | | **check-pr-template.yml** | SAFE | `AUTHOR` is passed via `env:` (not `${{ }}` in shell), double-quoted in comparison. `user.login` is GitHub-constrained to alphanumeric + hyphens. | **Actions are SHA-pinned** with version comments: `step-security/harden-runner@9af8...` (v2.19.4) and `actions/github-script@60a0...` (v7.0.1). ## Testing - [x] QA'd all new/changed functionality manually Since this is a GitHub Actions workflow that triggers on Dependabot PR opens, it cannot be fully end-to-end tested without a real Dependabot PR. The following offline validations were performed: ### 1. YAML syntax validation ``` $ ruby -e "require 'yaml'; YAML.safe_load(File.read('.github/workflows/dependabot-qa-ticket.yml')); puts 'YAML: valid'" YAML: valid ``` ### 2. Verified hardcoded GraphQL IDs against the live GitHub API **Project board ID** (`PVT_kwDOBDAnic4A5q11`): ``` $ gh api graphql -f query='query { node(id: "PVT_kwDOBDAnic4A5q11") { ... on ProjectV2 { title number url } } }' -> title: ":help-qa", number: 85, url: "https://github.com/orgs/fleetdm/projects/85" ``` **Status field ID** (`PVTSSF_lADOBDAnic4A5q11zgubZ6Y`) and option ID (`f75ad846`): ``` $ gh api graphql -f query='query { node(id: "PVTSSF_lADOBDAnic4A5q11zgubZ6Y") { ... on ProjectV2SingleSelectField { name options { id name } } } }' -> field name: "Status", option f75ad846 = "Ready" ✅ ``` ### 3. Verified assignee and label exist ``` $ gh api users/AndreyKizimenko --jq '.login' -> AndreyKizimenko ✅ $ gh label list --search dependabot -> dependabot ✅ ``` ### 4. Branch filtering logic unit tested Extracted the JavaScript filter logic and ran it against 8 test cases covering single-package updates, grouped updates, and exclusions: ``` $ node -e "<test script>" PASS | dependabot/go_modules/golang.org/x/crypto-0.24.0 | match (Go single) PASS | dependabot-go_modules-google.golang.org-grpc-1.82.1 | match (Go grouped) PASS | dependabot/npm_and_yarn/typescript-5.5.0 | match (npm single) PASS | dependabot-npm_and_yarn-postcss-8.5.18 | match (npm grouped) PASS | dependabot/npm_and_yarn/website/next-14.0.0 | skip (website single) PASS | dependabot-npm_and_yarn-website-next-14.0.0 | skip (website grouped) PASS | dependabot/npm_and_yarn/fleet-slackbot/axios-1.7.0 | skip (slackbot single) PASS | dependabot/github_actions/actions/checkout-4 | skip (GitHub Actions) 8/8 passed ``` ### 5. Validated filter against real Dependabot PRs Checked the 20 most recent Dependabot PRs (`gh pr list --author 'app/dependabot' --state all --limit 20`) to confirm real branch names match the filter patterns. This revealed three bugs that were fixed: 1. **Go branch prefix was wrong**: Real branches use `dependabot/go_modules/`, not `dependabot/gomod/`. Fixed. 2. **Grouped updates use dashes**: Real grouped branches like `dependabot-go_modules-...` and `dependabot-npm_and_yarn-...` were not matched. Added dash-variant prefix checks and exclusions. 3. **Status label mismatch**: The constant was named `AWAITING_QA` but the option ID maps to "Ready" on the board (there is no "Awaiting QA" status). Renamed to `READY` and updated comments. ### 6. CI checks All CI checks pass (CodeQL, zizmor, dependency-review, build-binaries, publish). **Note:** The project board integration requires a `FLEET_GITHUB_TOKEN_PROJECTS` repo secret with Organization Projects read/write scope. Without it, the issue is still created but not added to the board (a warning is logged). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an automated GitHub Actions workflow for eligible Dependabot update pull requests. * Creates (and deduplicates) labeled QA issues with pull request details and checklist content, and posts the issue back to the pull request. * Optionally links new QA issues to project tracking with an “Awaiting QA” status when configured. * **Chores** * Hardened the automation workflow’s runner and permissions. * Updated safety-gate configuration to waive checks for the new Dependabot QA workflow. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Github Actions
Fleet uses Github Actions for continuous integration (CI). This document describes best practices and at patterns for writing and maintaining Fleet's Github Actions workflows.
Bash
By default, Github Actions sets the shell to bash -e for linux and MacOS runners. To help write
safer bash scripts in run jobs and avoid common issues, override the default by adding the following
to the workflow file
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
By specifying the default shell to bash, some extra flags are set. The option pipefail changes
the behaviour when using the pipe | operator such that if any command in a pipeline fails, that
commands return code will be used a the return code for the whole pipeline. Consider the following
example in test-go.yaml
- name: Run Go Tests
run: |
# omitted ...
make test-go 2>&1 | tee /tmp/gotest.log
If the pipefail option was not set, this job would always succeed because tee would always
return success. This is not the intended behavior. Instead, we want the job to fail if make test-go fails.
Concurrency
Github Action runners are limited. If a lot of workflows are queued, they will wait in pending until a runner becomes available. This has caused issue in the past where workflows take an excessively long time to start. To help with this issue, use the following in workflows
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
cancel-in-progress: true
When a workflow is triggered via a pull request, it will cancel previous running workflows for that
pull request. This is especially useful when changes are pushed to a pull request frequently.
Manually triggered workflows, workflows that run on a schedule, and workflows triggered by pushes to
main are unaffected.