chore: import upstream snapshot with attribution
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:02:43 +08:00
commit d083df1fdb
731 changed files with 122439 additions and 0 deletions
@@ -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.`);
+57
View File
@@ -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 }}'
+26
View File
@@ -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
+16
View File
@@ -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 }}
+64
View File
@@ -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
+36
View File
@@ -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 }}
+26
View File
@@ -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
+34
View File
@@ -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'
+24
View File
@@ -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
+35
View File
@@ -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
+33
View File
@@ -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