Skip to content
Latchkey

Auto-triage scan output workflow (santifer/career-ops)

The Auto-triage scan output workflow from santifer/career-ops, explained and optimized by Latchkey.

A

CI health: A - excellent

Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: santifer/career-ops.github/workflows/auto-triage-scan-output.ymlLicense MITView source

What it does

This is the Auto-triage scan output workflow from the santifer/career-ops repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Auto-triage scan output

# career-ops runs locally. Some users point their own scheduled automation at
# this repo by mistake and file their personal scan results / run reports as
# issues here. Those are created via the API / `gh issue create`, which bypasses
# the issue templates and `blank_issues_enabled: false` entirely - so only a
# workflow can catch them. This one explains, labels, and closes them: it
# protects users' personal job-search data and keeps the public tracker clean.
#
# Deliberately CONSERVATIVE - it acts only when BOTH are true:
#   (a) the issue did NOT come through a template (no Code of Conduct checkbox), and
#   (b) it carries an unmistakable automated-report marker (boilerplate the
#       scheduled-scan reports ship with - not merely the word "scan", which
#       legitimate scanner-provider features use).
# It never locks the issue, so a real report can always be reopened.

on:
  issues:
    types: [opened, reopened]

permissions:
  issues: write

jobs:
  triage:
    runs-on: ubuntu-latest
    # Never auto-close issues opened by the owner or org members.
    if: ${{ github.event.issue.author_association != 'OWNER' && github.event.issue.author_association != 'MEMBER' }}
    steps:
      - uses: actions/github-script@v9
        with:
          script: |
            const issue = context.payload.issue;
            const body = issue.body || '';
            const hay = `${issue.title || ''}\n${body}`;

            // Gate A - template-created issues carry the CoC checkbox. Real
            // contributions always come through a template, so never touch them.
            if (/Code of Conduct/i.test(body)) return;

            // Gate B - must carry an unmistakable automated-report marker.
            const markers = [
              /Generated by career-ops automated routine/i,
              /Scheduled Portal Scan/i,
              /Scheduled Run Report/i,
              /Automated [Ss]can( (Result|Failed))?/i,
              /Auto-scan Results/i,
              /Daily AI Job Scan/i,
              /Morning (Briefing|Automation Run)/i,
              /Nightly Graphify/i,
              /This is an automated scan result/i,
              /onboarding (required|not complete)/i,
              /Gmail MCP token/i,
              /new offers added to pipeline/i,
            ];
            if (!markers.some((re) => re.test(hay))) return;

            const { owner, repo } = context.repo;
            const issue_number = issue.number;

            await github.rest.issues.createComment({
              owner, repo, issue_number,
              body: [
                `Hey @${issue.user.login} - this looks like automated output from your own career-ops setup landing on the upstream repo. πŸ‘‹`,
                ``,
                `career-ops runs **entirely locally**: your pipeline, scans, and reports live on your own machine (\`data/\`, \`reports/\`) - they never need to be filed here, and posting them upstream puts personal job-search data on a public tracker. Closing to keep your data private and the tracker tidy.`,
                ``,
                `If you have a scheduled run that opens issues, point it at your **own fork** or a private repo - not \`${owner}/${repo}\`. Real bugs are always welcome via the bug-report template. Thanks for building with career-ops! πŸš€`,
              ].join('\n'),
            });

            try {
              await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['scan-output'] });
            } catch (e) {
              core.info(`label add skipped: ${e.message}`);
            }

            await github.rest.issues.update({ owner, repo, issue_number, state: 'closed', state_reason: 'not_planned' });
            core.info(`Closed #${issue_number} as scan-output.`);

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: Auto-triage scan output
 
# career-ops runs locally. Some users point their own scheduled automation at
# this repo by mistake and file their personal scan results / run reports as
# issues here. Those are created via the API / `gh issue create`, which bypasses
# the issue templates and `blank_issues_enabled: false` entirely - so only a
# workflow can catch them. This one explains, labels, and closes them: it
# protects users' personal job-search data and keeps the public tracker clean.
#
# Deliberately CONSERVATIVE - it acts only when BOTH are true:
#   (a) the issue did NOT come through a template (no Code of Conduct checkbox), and
#   (b) it carries an unmistakable automated-report marker (boilerplate the
#       scheduled-scan reports ship with - not merely the word "scan", which
#       legitimate scanner-provider features use).
# It never locks the issue, so a real report can always be reopened.
 
on:
  issues:
    types: [opened, reopened]
 
permissions:
  issues: write
 
jobs:
  triage:
    timeout-minutes: 30
    runs-on: latchkey-small
    # Never auto-close issues opened by the owner or org members.
    if: ${{ github.event.issue.author_association != 'OWNER' && github.event.issue.author_association != 'MEMBER' }}
    steps:
      - uses: actions/github-script@v9
        with:
          script: |
            const issue = context.payload.issue;
            const body = issue.body || '';
            const hay = `${issue.title || ''}\n${body}`;
 
            // Gate A - template-created issues carry the CoC checkbox. Real
            // contributions always come through a template, so never touch them.
            if (/Code of Conduct/i.test(body)) return;
 
            // Gate B - must carry an unmistakable automated-report marker.
            const markers = [
              /Generated by career-ops automated routine/i,
              /Scheduled Portal Scan/i,
              /Scheduled Run Report/i,
              /Automated [Ss]can( (Result|Failed))?/i,
              /Auto-scan Results/i,
              /Daily AI Job Scan/i,
              /Morning (Briefing|Automation Run)/i,
              /Nightly Graphify/i,
              /This is an automated scan result/i,
              /onboarding (required|not complete)/i,
              /Gmail MCP token/i,
              /new offers added to pipeline/i,
            ];
            if (!markers.some((re) => re.test(hay))) return;
 
            const { owner, repo } = context.repo;
            const issue_number = issue.number;
 
            await github.rest.issues.createComment({
              owner, repo, issue_number,
              body: [
                `Hey @${issue.user.login} - this looks like automated output from your own career-ops setup landing on the upstream repo. πŸ‘‹`,
                ``,
                `career-ops runs **entirely locally**: your pipeline, scans, and reports live on your own machine (\`data/\`, \`reports/\`) - they never need to be filed here, and posting them upstream puts personal job-search data on a public tracker. Closing to keep your data private and the tracker tidy.`,
                ``,
                `If you have a scheduled run that opens issues, point it at your **own fork** or a private repo - not \`${owner}/${repo}\`. Real bugs are always welcome via the bug-report template. Thanks for building with career-ops! πŸš€`,
              ].join('\n'),
            });
 
            try {
              await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['scan-output'] });
            } catch (e) {
              core.info(`label add skipped: ${e.message}`);
            }
 
            await github.rest.issues.update({ owner, repo, issue_number, state: 'closed', state_reason: 'not_planned' });
            core.info(`Closed #${issue_number} as scan-output.`);
 

What changed

This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow