Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions .github/workflows/dependabot-qa-ticket.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
name: Create QA ticket for Dependabot PRs

# When Dependabot opens a code-dependency PR, create a QA issue on the
# #g-orchestration board so the team can verify that the bump introduces no
# behavioral regressions (see postmortem for #45763).

on:
pull_request_target:
types: [opened]
Comment thread
sharon-fdm marked this conversation as resolved.

permissions:
contents: read

jobs:
create-qa-ticket:
if: >
github.event.repository.owner.login == 'fleetdm' &&
github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Create QA issue and add to project board
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
PROJECT_TOKEN: ${{ secrets.FLEET_GITHUB_TOKEN_PROJECTS }}
with:
script: |
const pr = context.payload.pull_request;
const branch = pr.head.ref;

// Only process code dependency updates (Go modules, npm root).
// Skip: github-actions, docker, website npm, slackbot npm.
// Dependabot uses slashes for single-package updates and dashes for grouped updates:
// single: dependabot/go_modules/golang.org/x/crypto-0.35.0
// grouped: dependabot-go_modules-google.golang.org-grpc-1.82.1
const isGomod = branch.startsWith('dependabot/go_modules/')
|| branch.startsWith('dependabot-go_modules-');
const isNpm = (branch.startsWith('dependabot/npm_and_yarn/')
|| branch.startsWith('dependabot-npm_and_yarn-'))
&& !branch.includes('/website/')
&& !branch.includes('/fleet-slackbot/')
&& !branch.includes('-website-')
&& !branch.includes('-fleet-slackbot-');

if (!isGomod && !isNpm) {
console.log(`Skipping non-code dependency PR (branch: ${branch})`);
return;
}

const ecosystem = branch.includes('go_modules') ? 'Go' : 'npm';
const issueTitle = `QA: ${pr.title}`;

// Idempotency: check if a QA issue already exists for this PR
// (guards against duplicate tickets on workflow re-runs).
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'dependabot',
state: 'open',
per_page: 100,
});
const duplicate = existing.data.find(i => i.title === issueTitle);
if (duplicate) {
console.log(`QA issue already exists: #${duplicate.number} — skipping.`);
return;
}

// Create the QA tracking issue.
const issue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: issueTitle,
body: [
`## Dependabot dependency update — QA review needed`,
``,
`| Field | Value |`,
`|-------|-------|`,
`| **PR** | #${pr.number} |`,
`| **Ecosystem** | ${ecosystem} |`,
`| **Branch** | \`${branch}\` |`,
``,
`### Why this needs QA`,
``,
`Dependabot PRs update third-party dependencies. Even minor/patch bumps can`,
`change runtime behavior (see #45763 for a real example where a dependency`,
`bump silently broke SCEP certificate issuance). Please verify that the`,
`updated dependency does not cause regressions in related functionality.`,
``,
`### What to check`,
``,
`1. Review the PR diff and the dependency's changelog/release notes`,
`2. Identify which Fleet features use this dependency`,
`3. Test those features for behavioral changes`,
``,
`---`,
`*This issue was automatically created by the dependabot-qa-ticket workflow.*`,
].join('\n'),
labels: ['dependabot'],
assignees: ['AndreyKizimenko'],
});
Comment thread
sharon-fdm marked this conversation as resolved.

console.log(`Created issue #${issue.data.number}`);

// Comment on the PR linking to the QA issue.
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `QA ticket created: #${issue.data.number}`,
});

// --- Project board: add to #g-orchestration and set status to "Inbox" ---
// Requires a PAT stored as FLEET_GITHUB_TOKEN_PROJECTS with
// read/write access to organization projects.
const projectToken = process.env.PROJECT_TOKEN;
if (!projectToken) {
core.warning(
'FLEET_GITHUB_TOKEN_PROJECTS secret is not configured — ' +
'issue was created but not added to the project board. ' +
'Create a fine-grained PAT with Organization Projects read/write scope.'
);
return;
}

const { getOctokit } = require('@actions/github');
const projectOctokit = getOctokit(projectToken);

const PROJECT_ID = 'PVT_kwDOBDAnic4A4BEx'; // #g-orchestration (#71)
const STATUS_FIELD = 'PVTSSF_lADOBDAnic4A4BExzgtDlHw';
const INBOX = 'f33a1ec5';

// Add the issue to the project.
const addResult = await projectOctokit.graphql(`
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item { id }
}
}
`, {
projectId: PROJECT_ID,
contentId: issue.data.node_id,
});

const itemId = addResult.addProjectV2ItemById.item.id;
console.log(`Added to #g-orchestration board (item ${itemId})`);

// Set status to "Inbox".
await projectOctokit.graphql(`
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {singleSelectOptionId: $optionId}
}) {
projectV2Item { id }
}
}
`, {
projectId: PROJECT_ID,
itemId: itemId,
fieldId: STATUS_FIELD,
optionId: INBOX,
});

console.log('Status set to "Inbox"');
12 changes: 12 additions & 0 deletions .github/zizmor-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ rules:
# NOTE: `action.yml` matches every composite-action manifest. Today only .github/actions/r2-upload
# carries findings; a future finding in another composite action (e.g. eng-metrics) would also be
# suppressed. Revisit if more composite actions are added.
# dependabot-qa-ticket.yml uses pull_request_target for write access but never checks out
# PR code -- it only calls the GitHub API via actions/github-script, so there is no code-
# injection vector.
dangerous-triggers:
ignore:
- dependabot-qa-ticket.yml
# dependabot-qa-ticket.yml gates on github.event.pull_request.user.login (the PR author),
# not github.actor. zizmor still flags the pull_request_target bot condition; waived because
# user.login is not spoofable for same-repo Dependabot PRs.
bot-conditions:
ignore:
- dependabot-qa-ticket.yml
template-injection:
ignore:
- action.yml # .github/actions/r2-upload/action.yml (bash; env/inputs from trusted callers)
Expand Down
Loading