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
44 changes: 44 additions & 0 deletions .github/workflows/apply-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Cheap apply of curated notes onto a GitHub Release.
# Reads branch release-note-<semver> or vars RELEASE_NOTES +
# RELEASE_NOTES_TAG. Deletes the notes branch after a successful
# apply. Does not compile and does not start vsce/ovsx.
name: Apply release notes

on:
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. patchloom-v0.7.0)"
required: true
type: string

concurrency:
group: apply-release-notes-${{ inputs.tag }}
cancel-in-progress: false

permissions:
contents: read

jobs:
apply:
name: Apply notes
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ inputs.tag }}
RELEASE_NOTES: ${{ vars.RELEASE_NOTES }}
RELEASE_NOTES_TAG: ${{ vars.RELEASE_NOTES_TAG }}
steps:
- name: Harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Apply curated notes
run: bash scripts/apply-release-notes.sh
45 changes: 39 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: ci

# Recipe A: product compile stays on PR / merge_group / dispatch.
# Do not add push to main (that is a second matrix). Scorecard default-branch
# runs stay in post-merge.yml. Recipe G: release-please* heads skip the
# 3-OS + integration + UI matrix and run a cheap ubuntu tsc instead.
on:
push:
branches: [main]
pull_request:
merge_group:
workflow_dispatch:
Expand Down Expand Up @@ -44,7 +46,10 @@ jobs:

unit-test:
needs: [changes]
if: always() && (needs.changes.result == 'skipped' || needs.changes.outputs.code == 'true')
if: >
always()
&& (needs.changes.result == 'skipped' || needs.changes.outputs.code == 'true')
&& (github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please'))
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -96,7 +101,7 @@ jobs:
fi

- name: Update coverage badge
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
if: matrix.os == 'ubuntu-latest' && github.event_name == 'workflow_dispatch'
continue-on-error: true
uses: schneegans/dynamic-badges-action@28b0fa8bdeb46170ac397105ece0c1fe58f68910 # v1.9.0
with:
Expand All @@ -109,7 +114,10 @@ jobs:

build:
needs: [changes]
if: always() && (needs.changes.result == 'skipped' || needs.changes.outputs.code == 'true')
if: >
always()
&& (needs.changes.result == 'skipped' || needs.changes.outputs.code == 'true')
&& (github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please'))
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down Expand Up @@ -139,6 +147,7 @@ jobs:
&& (needs.changes.result == 'skipped' || needs.changes.outputs.code == 'true')
&& (needs.unit-test.result == 'success' || needs.unit-test.result == 'skipped')
&& (needs.build.result == 'success' || needs.build.result == 'skipped')
&& (github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please'))
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -185,6 +194,29 @@ jobs:
if: runner.os != 'Linux'
run: npx extest run-tests './out-uitest/test/ui/*.test.js' --extensions_dir .vscode-test/extensions

# Recipe G: version-bump PRs already ran the full matrix on the feature PR.
# Keep a cheap ubuntu compile so a broken package.json / tsconfig still fails.
release-please-check:
if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release-please')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup Node.js
uses: ./.github/actions/setup-node

- name: Typecheck (release-please stand-in)
run: npx tsc --noEmit

ci:
# Final gate job (if: always()). This is the recommended pattern (see patchloom
# and attune) so that individual matrix jobs can use the changes filter without
Expand All @@ -194,12 +226,13 @@ jobs:
- unit-test
- build
- integration-test
- release-please-check
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: All CI jobs passed (or were correctly skipped for docs-only change)
run: |
results=("${{ needs.unit-test.result }}" "${{ needs.build.result }}" "${{ needs.integration-test.result }}")
results=("${{ needs.unit-test.result }}" "${{ needs.build.result }}" "${{ needs.integration-test.result }}" "${{ needs.release-please-check.result }}")
for r in "${results[@]}"; do
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
echo "FAILED: dependency reported '$r'"
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ name: FOSSA license scan
on:
push:
branches: [main]
paths:
- "package.json"
- "package-lock.json"
- "src/**"
- ".github/workflows/fossa.yml"
pull_request:
paths:
- "package.json"
- "package-lock.json"
- "src/**"
- ".github/workflows/fossa.yml"
workflow_dispatch:

permissions:
Expand All @@ -16,6 +26,7 @@ concurrency:
jobs:
fossa:
name: License compliance
if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please')
env:
FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }}
runs-on: ubuntu-latest
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/post-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ jobs:
with:
egress-policy: audit

- name: Trigger workflows on main
# Recipe A: do not dispatch ci.yml or security.yml on a green squash.
# The feature PR (or merge_group) already ran those. A second matrix
# is waste. Scorecard wants a default-branch run; keep that dispatch.
# Do not set require-up-to-date to justify dropping this file (#156).
- name: Trigger Scorecard on main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run ci.yml --ref main --repo "${{ github.repository }}"
gh workflow run security.yml --ref main --repo "${{ github.repository }}"
gh workflow run scorecard.yml --ref main --repo "${{ github.repository }}"

- name: Auto-update open PR branches
Expand Down
13 changes: 6 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,14 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ needs.release-please.outputs.tag_name }}" patchloom.vsix
- name: Apply custom release notes
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ -f RELEASE_NOTES.md ]; then
echo "Custom release notes found, updating release body..."
gh release edit "${{ needs.release-please.outputs.tag_name }}" --notes-file RELEASE_NOTES.md
else
echo "No custom release notes, using auto-generated notes"
fi
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.release-please.outputs.tag_name }}
RELEASE_NOTES: ${{ vars.RELEASE_NOTES }}
RELEASE_NOTES_TAG: ${{ vars.RELEASE_NOTES_TAG }}
run: bash scripts/apply-release-notes.sh
# See ~/.grok/skills/vsce-publish/SKILL.md (or /vsce-publish) for the full
# one-time browser setup (aex.dev.azure.com, PAT scopes, Open VSX agreement
# + create-namespace + ownership claim), secret names, and troubleshooting.
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: security

# Recipe A: no third CodeQL on every push to main. PR / merge_group
# already scanned the tree. Weekly schedule keeps the default branch warm.
on:
push:
branches: [main]
pull_request:
merge_group:
schedule:
Expand Down Expand Up @@ -97,27 +97,38 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Release-please stand-in
if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release-please')
run: echo 'version-bump PR; CodeQL already ran on the feature PR'

- name: Harden runner
if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please')
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit

- name: Checkout
if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please')
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Initialize CodeQL
if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please')
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
languages: javascript-typescript,actions

- name: Autobuild
if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please')
uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9

- name: Perform CodeQL analysis
if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-please')
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9

dependency-review:
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ test/
scripts/
hide-test-vscode.sh macOS: patch test VS Code to suppress window activation
guard-no-release-merge.sh Hard abort if agent tries to merge a release PR without approval
apply-release-notes.sh Apply release-note-<semver> (or legacy RELEASE_NOTES.md) onto a GitHub Release
.github/
CODEOWNERS Owner for all files
copilot-instructions.md Pointer to AGENTS.md for GitHub Copilot
Expand All @@ -92,7 +93,8 @@ scripts/
fossa.yml FOSSA license compliance scan
links.yml Markdown link checker (lychee)
pat-expiry.yml Monthly VSCE PAT expiry reminder
post-merge.yml Trigger CI/security/scorecard on main after auto-merge
post-merge.yml Scorecard dispatch after squash (not ci/security)
apply-release-notes.yml Cheap apply of release-note-<semver> onto a GitHub Release
pr-title.yml Semantic PR title validation
scorecard.yml OpenSSF Scorecard analysis (weekly + on push)
release.yml Release: release-please + .vsix packaging and upload
Expand Down
119 changes: 119 additions & 0 deletions scripts/apply-release-notes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# Apply curated notes to an existing GitHub Release, then delete the
# notes branch. Notes are not on main.
#
# Sources, first match:
# 1. NOTES_FILE (tests)
# 2. RELEASE_NOTES.md on branch release-note-<semver>
# (tag patchloom-v0.6.0 or v0.6.0 -> release-note-0.6.0)
# 3. Actions vars RELEASE_NOTES + RELEASE_NOTES_TAG (tag must match)
# 4. RELEASE_NOTES.md in the current checkout (legacy main-file path)
#
# Missing source is a no-op (auto changelog stays). After a successful
# apply from the notes branch, that branch is deleted. Variables are
# left in place; the tag pin stops them applying to a later cut.
set -euo pipefail

TAG="${TAG:-}"
REPO="${GH_REPO:-${GITHUB_REPOSITORY:-}}"
DRY_RUN="${DRY_RUN:-0}"
DELETE_BRANCH="${DELETE_BRANCH:-1}"
NOTES_FILE="${NOTES_FILE:-}"
RELEASE_NOTES="${RELEASE_NOTES:-}"
RELEASE_NOTES_TAG="${RELEASE_NOTES_TAG:-}"

semver=""
if [[ "${TAG}" =~ ^patchloom-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
semver="${TAG#patchloom-v}"
elif [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
semver="${TAG#v}"
else
echo "TAG must look like patchloom-vX.Y.Z or vX.Y.Z: ${TAG}" >&2
exit 1
fi

if [ -z "${REPO}" ]; then
echo "GH_REPO or GITHUB_REPOSITORY required" >&2
exit 1
fi

branch="${NOTES_BRANCH:-release-note-${semver}}"
loaded_from_branch=0
source_label=""

tmp="$(mktemp)"
cleanup() { rm -f "${tmp}"; }
trap cleanup EXIT

tag_matches_pin() {
local pin="$1"
if [ -z "${pin}" ]; then
return 1
fi
if [ "${pin}" = "${TAG}" ] || [ "${pin}" = "${semver}" ] \
|| [ "${pin}" = "v${semver}" ] || [ "${pin}" = "patchloom-v${semver}" ]; then
return 0
fi
return 1
}

if [ -n "${NOTES_FILE}" ] && [ -f "${NOTES_FILE}" ]; then
echo "PLAN: file ${NOTES_FILE}"
cp "${NOTES_FILE}" "${tmp}"
source_label="file:${NOTES_FILE}"
elif [ -n "${GH_TOKEN:-}" ]; then
echo "PLAN: fetch RELEASE_NOTES.md from ${branch}"
if gh api "repos/${REPO}/contents/RELEASE_NOTES.md?ref=${branch}" \
-H "Accept: application/vnd.github.raw" >"${tmp}"; then
loaded_from_branch=1
source_label="branch:${branch}"
else
: >"${tmp}"
echo "PLAN: no notes branch ${branch}"
fi
fi

if [ ! -s "${tmp}" ] && [ -n "${RELEASE_NOTES}" ] \
&& tag_matches_pin "${RELEASE_NOTES_TAG}"; then
echo "PLAN: Actions variable RELEASE_NOTES (pin ${RELEASE_NOTES_TAG})"
printf '%s\n' "${RELEASE_NOTES}" >"${tmp}"
source_label="variable"
fi

if [ ! -s "${tmp}" ] && [ -f RELEASE_NOTES.md ]; then
echo "PLAN: legacy RELEASE_NOTES.md in checkout"
cp RELEASE_NOTES.md "${tmp}"
source_label="file:RELEASE_NOTES.md"
fi

if [ ! -s "${tmp}" ]; then
echo "OK: no curated notes for ${TAG}; leaving auto notes"
exit 0
fi

if [ "${DRY_RUN}" = "1" ]; then
echo "DRY_RUN: would apply ${source_label} to ${TAG}"
echo "BYTES: $(wc -c <"${tmp}" | tr -d ' ')"
if [ "${loaded_from_branch}" = "1" ] && [ "${DELETE_BRANCH}" = "1" ]; then
echo "DRY_RUN: would delete branch ${branch}"
fi
exit 0
fi

if ! gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then
echo "FAIL: release ${TAG} does not exist" >&2
exit 1
fi

echo "DO: gh release edit ${TAG} from ${source_label}"
gh release edit "${TAG}" --repo "${REPO}" --notes-file "${tmp}"
echo "OK: applied notes to ${TAG}"

if [ "${loaded_from_branch}" = "1" ] && [ "${DELETE_BRANCH}" = "1" ]; then
echo "DO: delete ${branch}"
if gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}"; then
echo "DONE: deleted ${branch}"
else
echo "WARN: could not delete ${branch}; delete it by hand" >&2
fi
fi
Loading
Loading