diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml new file mode 100644 index 0000000..ea83d66 --- /dev/null +++ b/.github/workflows/commits.yml @@ -0,0 +1,32 @@ +name: Commits + +# The changelog and release notes are generated from commit subjects, so the +# format is enforced rather than encouraged. A commit that does not parse is a +# change that vanishes from the release notes. +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: commits-${{ github.ref }} + cancel-in-progress: true + +jobs: + conventional: + name: Conventional commits + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history so the base..head range can be walked. + fetch-depth: 0 + + - name: Validate commit subjects + run: | + bash scripts/check-conventional-commits.sh \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3e8d1db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +# Publishing a release is a deliberate act: push a tag, and this turns the +# conventional commits since the previous tag into grouped release notes. +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Existing tag to (re)build notes for" + required: true + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Publish release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Build release notes + id: notes + run: bash scripts/build-release-notes.sh "${{ inputs.tag || github.ref_name }}" > NOTES.md + + - name: Create or update the GitHub Release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.tag || github.ref_name }} + run: | + if gh release view "$TAG" >/dev/null 2>&1; then + gh release edit "$TAG" --notes-file NOTES.md + else + gh release create "$TAG" --title "$TAG" --notes-file NOTES.md + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2ef084e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +Notable changes to JobInsight. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); +versions follow [Semantic Versioning](https://semver.org/). + +Entries from v0.1.0 onward are generated from Conventional Commits by +`scripts/build-release-notes.sh`. + +## [Unreleased] + +Nothing yet. + +## [0.1.0] — 2026-09-15 + +First tagged release. The extension version in `manifest.json` and the tag now +agree, so "which build is this" has an answer. + +### Added + +- Chrome MV3 extension that analyses LinkedIn job postings in place: experience, + education, sponsorship, US citizenship or clearance, a short summary, and + highlighted keywords +- Draggable, resizable overlay; results cached for seven days +- Backend proxy holding the OpenAI key behind Google OAuth, so the key never + ships in extension source +- `/api/health`, advertised by the root document + +### Infrastructure + +- CI on Node 20 and 22 covering the proxy's origin allowlist: requests with no + Origin, from another extension id, and from an ordinary web page are refused; + the configured extension is not; an oversized body returns 413 rather than + being proxied +- `scripts/validate-manifest.mjs` checks the manifest parses, declares MV3, and + that every file it references exists — a manifest naming a renamed file loads + as a broken extension and nothing else catches it +- `npm audit` at high severity, and a syntax check over every extension script +- Conventional commits enforced; releases generated from them + +[Unreleased]: https://github.com/bibeshpyakurel/JobInsight/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/bibeshpyakurel/JobInsight/releases/tag/v0.1.0 diff --git a/scripts/build-release-notes.sh b/scripts/build-release-notes.sh new file mode 100755 index 0000000..ce32650 --- /dev/null +++ b/scripts/build-release-notes.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Turn the conventional commits between the previous tag and this one into +# grouped Markdown release notes. +set -euo pipefail + +TAG="${1:?tag required}" +PREV=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true) + +if [ -n "$PREV" ]; then + RANGE="${PREV}..${TAG}" + echo "Changes since **${PREV}**." +else + RANGE="$TAG" + echo "First tagged release. Everything below is the history leading up to **${TAG}**." +fi +echo "" + +emit() { # + local heading="$1" types="$2" body + body=$(git log --no-merges --format='%s|%h' "$RANGE" \ + | grep -E "^(${types})(\([a-z0-9._/-]+\))?!?: " || true) + [ -z "$body" ] && return 0 + echo "### ${heading}" + echo "" + while IFS='|' read -r subject sha; do + # strip the type prefix; keep any scope as a bold lead-in + text=$(echo "$subject" | sed -E 's/^[a-z]+(\([a-z0-9._\/-]+\))?!?: //') + scope=$(echo "$subject" | sed -nE 's/^[a-z]+\(([a-z0-9._\/-]+)\)!?: .*/\1/p') + if [ -n "$scope" ]; then + echo "- **${scope}**: ${text} (\`${sha}\`)" + else + echo "- ${text} (\`${sha}\`)" + fi + done <<< "$body" + echo "" +} + +breaking=$(git log --no-merges --format='%s|%h' "$RANGE" | grep -E '^[a-z]+(\([a-z0-9._/-]+\))?!: ' || true) +if [ -n "$breaking" ]; then + echo "### Breaking changes" + echo "" + while IFS='|' read -r subject sha; do + echo "- ${subject} (\`${sha}\`)" + done <<< "$breaking" + echo "" +fi + +emit "Features" "feat" +emit "Fixes" "fix" +emit "Performance" "perf" +emit "Documentation" "docs" +emit "Build and CI" "build|ci" +emit "Internal" "refactor|test|chore|style|revert" + +echo "---" +echo "" +echo "_Notes generated from Conventional Commits by \`scripts/build-release-notes.sh\`._" diff --git a/scripts/check-conventional-commits.sh b/scripts/check-conventional-commits.sh new file mode 100755 index 0000000..9bd6cb2 --- /dev/null +++ b/scripts/check-conventional-commits.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Validate that every commit introduced by a pull request uses Conventional +# Commits. The changelog is generated from these subjects, so a commit that +# does not parse is a commit that silently disappears from the release notes. +# +# Usage: check-conventional-commits.sh +set -euo pipefail + +BASE="${1:?base sha required}" +HEAD="${2:?head sha required}" + +TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" +# type(optional scope)!: summary — summary must be non-empty and lowercase-ish +PATTERN="^(${TYPES})(\([a-z0-9._/-]+\))?!?: .+" + +fail=0 +count=0 + +# Merge commits are generated by GitHub, not written by a human — skip them. +while IFS= read -r sha; do + [ -z "$sha" ] && continue + subject=$(git log -1 --format=%s "$sha") + count=$((count + 1)) + + if [[ "$subject" =~ $PATTERN ]]; then + if [ "${#subject}" -gt 72 ]; then + echo " ! ${sha:0:8} subject is ${#subject} chars (soft limit 72)" + echo " $subject" + else + echo " ok ${sha:0:8} $subject" + fi + else + echo " x ${sha:0:8} $subject" + fail=1 + fi +done < <(git rev-list --no-merges "${BASE}..${HEAD}") + +if [ "$count" -eq 0 ]; then + echo "No non-merge commits in range; nothing to validate." + exit 0 +fi + +if [ "$fail" -ne 0 ]; then + cat <<'MSG' + +Some commit subjects are not Conventional Commits. + + (): + + types: feat fix docs style refactor perf test build ci chore revert + breaking: append ! after the type, or add a BREAKING CHANGE: footer + + good: feat(log): accept workouts pasted as free text + fix(auth): stop refreshing an expired session on the server + bad: Update | changes | deployable | fixed stuff + +Rewrite them with: git rebase -i (reword the offending commits) +MSG + exit 1 +fi + +echo "" +echo "All $count commit(s) are Conventional Commits."