chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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,
|
||||
// Original shipped marker (unchanged): bare "Automated Scan"[ Result| Failed].
|
||||
/Automated [Ss]can( (Result|Failed))?/i,
|
||||
// Also catch the machine report header "## Automated <Weekly|Daily|Nightly|…> Scan"
|
||||
// (one interposed adjective). Header-anchored (`^#{1,6}\s+…`, multiline) so human issue
|
||||
// titles or prose that merely mention an "automated … scan" are NOT auto-closed — only
|
||||
// the generated markdown section header matches.
|
||||
/^#{1,6}\s+Automated (?:\w+ )?[Ss]can\b/im,
|
||||
// Emoji-led scan-digest title, e.g. "🔍 Scan results — <date> — N new leads" (#1791).
|
||||
/^\s*(?:🔍|🔎|📊|📋|🧭|📡|✨)\s*Scan\b/iu,
|
||||
/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.`);
|
||||
@@ -0,0 +1,57 @@
|
||||
name: 'CodeQL Analysis'
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1' # Weekly Monday 4am UTC
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript-typescript', 'go']
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Setup Go
|
||||
if: matrix.language == 'go'
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: dashboard/go.mod
|
||||
cache-dependency-path: dashboard/go.sum
|
||||
|
||||
- name: Build Go
|
||||
if: matrix.language == 'go'
|
||||
# go build skips *_test.go, so CodeQL's traced build never saw test
|
||||
# files (tool status showed Go at 13/26 files scanned). Compiling the
|
||||
# test packages without running them (-run '^$') lets the extractor
|
||||
# cover them too. The two windows-tagged files stay out by platform.
|
||||
run: |
|
||||
cd dashboard
|
||||
go build ./...
|
||||
go test -run '^$' ./...
|
||||
|
||||
- name: Autobuild
|
||||
if: matrix.language != 'go'
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: '/language:${{ matrix.language }}'
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Dependency Review
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Dependency Review
|
||||
# TODO: remove continue-on-error once the Dependency graph is enabled in
|
||||
# Settings > Security & Analysis (see #343). Until then, this is a temporary
|
||||
# workaround to unblock PRs — note that fail-on-severity: high is still
|
||||
# configured but will not block merges while continue-on-error is true.
|
||||
continue-on-error: true
|
||||
uses: actions/dependency-review-action@v5.0.0
|
||||
with:
|
||||
fail-on-severity: high
|
||||
comment-summary-in-pr: always
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Label PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,64 @@
|
||||
name: No user data in PRs
|
||||
|
||||
# Hard-blocks any PR that adds or modifies user-layer (private data) files.
|
||||
# These live ONLY on the candidate's machine per DATA_CONTRACT.md and must never
|
||||
# enter the repo — they are personal job-search data. Sample/fixture data belongs
|
||||
# under examples/. Tracked scaffolding (.gitkeep keepers, dir READMEs) is exempt.
|
||||
# Paths mirror USER_PATHS in update-system.mjs. Uses the API (listFiles) so it
|
||||
# works for fork PRs too.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pull_number = context.payload.pull_request.number;
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number, per_page: 100,
|
||||
});
|
||||
|
||||
// User-layer paths from DATA_CONTRACT.md and update-system.mjs.
|
||||
// voice-dna.md is omitted on purpose: it ships as a populated
|
||||
// system default, so edits to it are legit.
|
||||
const USER_PATHS = [
|
||||
/^cv\.md$/,
|
||||
/^config\/profile\.yml$/,
|
||||
/^modes\/_profile\.md$/,
|
||||
/^modes\/_custom\.md$/,
|
||||
/^portals\.yml$/,
|
||||
/^article-digest\.md$/,
|
||||
/^interview-prep\//,
|
||||
/^data\//,
|
||||
/^reports\//,
|
||||
/^output\//,
|
||||
/^jds\//,
|
||||
/^writing-samples\//,
|
||||
];
|
||||
// Tracked scaffolding that legitimately lives under those dirs.
|
||||
const isScaffold = (f) => /(^|\/)\.gitkeep$/.test(f) || /(^|\/)README\.md$/.test(f);
|
||||
|
||||
const bad = files
|
||||
.filter((f) => f.status !== 'removed')
|
||||
.map((f) => f.filename)
|
||||
.filter((f) => USER_PATHS.some((re) => re.test(f)) && !isScaffold(f));
|
||||
|
||||
if (bad.length) {
|
||||
core.setFailed(
|
||||
'This PR adds/modifies user-layer (private data) files. These live ONLY on your ' +
|
||||
'machine per DATA_CONTRACT.md and must never be committed — use examples/ for ' +
|
||||
'sample data.\nOffending files:\n ' + bad.join('\n ')
|
||||
);
|
||||
} else {
|
||||
core.info('OK — no user-layer (private data) files touched.');
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
name: 'Plugin registry validate'
|
||||
|
||||
# Runs on PRs that touch the curated plugin registry. Uses `pull_request` (NOT
|
||||
# pull_request_target), read-only permissions, and NO secrets — so cloning +
|
||||
# statically auditing a submitter's plugin can never reach a token or write the
|
||||
# repo. No plugin code is executed (the manifest is parsed, the audit is static).
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins-registry/**'
|
||||
- 'plugins-registry.json' # legacy single-file location (pre-migration forks)
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel superseded runs when the same PR is pushed again.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate registry entries
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# fetch-depth: 0 so the "One plugin per PR" diff below has the base
|
||||
# branch's history + merge-base available (a shallow checkout truncates it).
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
# Deterministic shape gate (no network).
|
||||
- name: Validate registry shape
|
||||
run: node validate-plugin-registry.mjs
|
||||
|
||||
# Deep gate: clone each entry at its pinned SHA + static-validate + audit.
|
||||
# No secrets are present in this job; nothing the plugin ships is executed.
|
||||
- name: Clone + audit each registry entry
|
||||
run: node validate-plugin-registry.mjs --deep
|
||||
|
||||
- name: One plugin per PR
|
||||
# base_ref goes through env, never ${{ }}-spliced into the script —
|
||||
# anything expanded inside run: is literal shell code (template injection).
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
# Make origin/$BASE_REF resolvable (checkout of a PR merge ref does not
|
||||
# create the base remote-tracking branch on its own).
|
||||
git fetch --no-tags origin "$BASE_REF"
|
||||
# One-time migration exemption: the PR that removes the legacy single
|
||||
# file necessarily rewrites every entry into plugins-registry/<id>.json.
|
||||
# This can only ever match the migration itself — once it lands,
|
||||
# plugins-registry.json no longer exists for any future PR to delete,
|
||||
# so the one-plugin gate below still holds for all registration PRs.
|
||||
if git diff --name-only --diff-filter=D "origin/$BASE_REF"...HEAD -- plugins-registry.json | grep -q .; then
|
||||
echo "Migration PR (removes legacy plugins-registry.json) — one-plugin-per-PR gate not applicable."
|
||||
exit 0
|
||||
fi
|
||||
CHANGED=$(git diff --name-only "origin/$BASE_REF"...HEAD -- 'plugins-registry/*.json' plugins-registry.json | wc -l)
|
||||
if [ "$CHANGED" -gt 1 ]; then echo "A registry PR may add or bump exactly one plugins-registry/<id>.json file"; exit 1; fi
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Release Please
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v5
|
||||
id: release
|
||||
with:
|
||||
config-file: release-please-config.json
|
||||
manifest-file: .release-please-manifest.json
|
||||
|
||||
# Publish the npm scaffolder only when a release was just created.
|
||||
# Runs in the same job because a release created via GITHUB_TOKEN does
|
||||
# not trigger separate `on: release` workflows.
|
||||
- uses: actions/checkout@v7
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
- uses: actions/setup-node@v6
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Publish scaffolder to npm
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
run: npm publish --provenance --access public
|
||||
working-directory: scaffolder
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,26 @@
|
||||
name: SBOM Generation
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sbom:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate SBOM (SPDX)
|
||||
uses: anchore/sbom-action@v0
|
||||
with:
|
||||
artifact-name: career-ops-sbom.spdx.json
|
||||
format: spdx-json
|
||||
output-file: career-ops-sbom.spdx.json
|
||||
|
||||
- name: Upload SBOM to release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh release upload ${{ github.event.release.tag_name }} career-ops-sbom.spdx.json
|
||||
@@ -0,0 +1,34 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday at 6am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
activity in 60 days. It will be closed in 14 days if no further activity occurs.
|
||||
If this is still relevant, please comment to keep it open.
|
||||
stale-pr-message: >
|
||||
This PR has been automatically marked as stale because it has not had
|
||||
activity in 30 days. It will be closed in 14 days if no further activity occurs.
|
||||
If you're still working on this, please rebase and comment to keep it open.
|
||||
close-issue-message: 'Closed due to inactivity. Feel free to reopen if this is still relevant.'
|
||||
close-pr-message: 'Closed due to inactivity. Feel free to reopen with a fresh rebase if you want to continue.'
|
||||
days-before-issue-stale: 60
|
||||
days-before-pr-stale: 30
|
||||
days-before-issue-close: 14
|
||||
days-before-pr-close: 14
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-issue-labels: 'pinned,security,roadmap'
|
||||
exempt-pr-labels: 'pinned,security'
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
# Skip the postinstall (npx playwright install chromium); the tests
|
||||
# never launch a browser, so the ~170 MB download is pure CI overhead.
|
||||
- run: npm install --ignore-scripts
|
||||
- run: node test-all.mjs --quick
|
||||
- name: Go dashboard tests
|
||||
run: go test ./...
|
||||
working-directory: dashboard
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Web CI
|
||||
|
||||
# CI for the experimental web UI (web/). Informative by design: it is NOT a
|
||||
# required check for the core — a red here never blocks a core merge, and core
|
||||
# changes never trigger it. The web/ tree versions and releases as its own
|
||||
# release-please component.
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'web/**'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
web:
|
||||
name: web typecheck + build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npx tsc --noEmit
|
||||
- run: npm run build
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Welcome
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/first-interaction@v3
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_message: |
|
||||
Welcome to career-ops, @${{ github.actor }}! Thanks for your first PR.
|
||||
|
||||
A few things to know:
|
||||
- Tests will run automatically — check the status below
|
||||
- Make sure you've linked a related issue (required for features)
|
||||
- Read [CONTRIBUTING.md](https://github.com/santifer/career-ops/blob/main/CONTRIBUTING.md) if you haven't
|
||||
|
||||
We'll review your PR soon. Join our [Discord](https://discord.gg/8pRpHETxa4) if you have questions.
|
||||
issue_message: |
|
||||
Thanks for opening your first issue, @${{ github.actor }}! We'll take a look soon.
|
||||
|
||||
In the meantime:
|
||||
- Check [SUPPORT.md](https://github.com/santifer/career-ops/blob/main/SUPPORT.md) for setup help
|
||||
- Join our [Discord](https://discord.gg/8pRpHETxa4) for quick answers
|
||||
Reference in New Issue
Block a user