Files
fleet/.github/workflows
Sharon Katz 0fd5739514 Auto-create QA tickets for Dependabot PRs (#48008)
**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 -->
2026-07-28 13:43:52 -04:00
..
2026-07-14 12:26:24 -03:00

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.