diff --git a/.github/release-please/.release-please-manifest.json b/.github/release-please/.release-please-manifest.json deleted file mode 100644 index e18ee0776..000000000 --- a/.github/release-please/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "0.0.0" -} diff --git a/.github/release-please/release-please-config.json b/.github/release-please/release-please-config.json deleted file mode 100644 index 9ca39d5d9..000000000 --- a/.github/release-please/release-please-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "packages": { - ".": { - "release-type": "simple", - "component": "Security Frameworks" - } - } -} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index 71ea4a829..000000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Please - -on: - push: - branches: [main] - -permissions: - contents: write - pull-requests: write - -jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Create or update release PR - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 - with: - target-branch: main - config-file: .github/release-please/release-please-config.json - manifest-file: .github/release-please/.release-please-manifest.json diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 000000000..f9366dc66 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,114 @@ +name: Release PR + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + version: + description: "Override version (e.g. v0.0.3). Leave blank to auto-bump." + required: false + type: string + +permissions: {} + +concurrency: + group: release-pr + cancel-in-progress: false + +jobs: + release-pr: + if: ${{ github.event_name == 'workflow_dispatch' || !contains(github.event.head_commit.message, 'chore(release):') }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + + - name: Compute next version + id: version + env: + OVERRIDE: ${{ inputs.version }} + run: | + set -euo pipefail + if [ -n "${OVERRIDE:-}" ]; then + if ! printf '%s' "$OVERRIDE" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Invalid version override '$OVERRIDE' (expected vMAJOR.MINOR.PATCH)" + exit 1 + fi + next="$OVERRIDE" + echo "Using override: $next" + else + latest=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n1 || true) + if [ -z "$latest" ]; then + next="v0.0.1" + else + v="${latest#v}" + major="${v%%.*}" + rest="${v#*.}" + minor="${rest%%.*}" + patch="${rest#*.}" + next="v${major}.${minor}.$((patch + 1))" + fi + echo "Latest tag: ${latest:-none}" + fi + echo "next=$next" >> "$GITHUB_OUTPUT" + echo "Next version: $next" + + - name: Ensure changelog exists + run: touch CHANGELOG.md + + - name: Generate changelog + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + id: git-cliff + with: + config: cliff.toml + github_token: ${{ secrets.GITHUB_TOKEN }} + args: --unreleased --tag ${{ steps.version.outputs.next }} --prepend CHANGELOG.md + env: + OUTPUT: RELEASE_NOTES.md + + - name: Check for releasable changes + id: check + run: | + set -euo pipefail + if grep -qE '^- ' RELEASE_NOTES.md; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "Changes found:" + cat RELEASE_NOTES.md + else + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "Nothing to release since the last tag. Skipping." + fi + + - name: Write VERSION + if: steps.check.outputs.has_changes == 'true' + env: + NEXT: ${{ steps.version.outputs.next }} + run: printf '%s\n' "$NEXT" > VERSION + + - name: Open release PR + if: steps.check.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sign-commits: true + base: main + branch: release/${{ steps.version.outputs.next }} + title: "chore(release): ${{ steps.version.outputs.next }}" + commit-message: "chore(release): ${{ steps.version.outputs.next }}" + labels: release + delete-branch: true + add-paths: | + CHANGELOG.md + VERSION + body: | + Automated release PR for **${{ steps.version.outputs.next }}**. + + Review the changelog, edit if needed, then merge. + Merging creates the tag and publishes the GitHub Release. \ No newline at end of file diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 000000000..340312933 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,61 @@ +name: Publish Release + +on: + pull_request: + types: [closed] + +permissions: {} + +jobs: + publish: + if: > + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'main' && + startsWith(github.event.pull_request.head.ref, 'release/') && + contains(github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + + - name: Read version + id: version + run: | + set -euo pipefail + tag="$(head -n1 VERSION | tr -d '[:space:]')" + if ! printf '%s' "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Invalid VERSION content '$tag' (expected vMAJOR.MINOR.PATCH)" + exit 1 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Extract latest changelog section + run: | + awk '/^## /{ if (found) exit; found=1; next } found' CHANGELOG.md > RELEASE_NOTES.md + cat RELEASE_NOTES.md + + - name: Create tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + sha="$(git rev-parse HEAD)" + gh api "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f "ref=refs/tags/${TAG}" \ + -f "sha=${sha}" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + gh release create "$TAG" \ + --title "$TAG" \ + --notes-file RELEASE_NOTES.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..d3f5a12fa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ + diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 000000000..3e6e86a5f --- /dev/null +++ b/cliff.toml @@ -0,0 +1,49 @@ +[changelog] +header = """ +# Changelog + +All notable changes to the Security Frameworks are documented here. +""" + +body = """ +{% macro remote_url() %}https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}{% endmacro %} +{% if version %} +## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %} +## Unreleased +{% endif %} +{% for group, commits in commits | filter(attribute="merge_commit", value=false) | group_by(attribute="group") %} +### {{ group | striptags | trim | upper_first }} +{% for commit in commits %} +{%- if commit.remote.pr_title -%} +{%- set entry = commit.remote.pr_title -%} +{%- else -%} +{%- set entry = commit.message | split(pat="\n") | first -%} +{%- endif %} +- {{ entry | trim }}{% if commit.remote.pr_number %} ([#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})){% endif %}{% if commit.remote.username %} by @{{ commit.remote.username }}{% endif %} +{%- endfor %} +{% endfor %} +""" + +trim = true + +[git] +conventional_commits = false +filter_unconventional = false +filter_commits = false +sort_commits = "oldest" + +commit_parsers = [ + { message = "^Merge pull request", skip = true }, + { message = "^Merge branch", skip = true }, + { message = "^chore\\(release\\)", skip = true }, + + { field = "github.pr_labels", pattern = "dependencies", skip = true }, + + { field = "github.pr_labels", pattern = "content:add", group = " New content" }, + { field = "github.pr_labels", pattern = "content:update", group = " Updated content" }, + { field = "github.pr_labels", pattern = "enhancement", group = " Enhancements" }, + { field = "github.pr_labels", pattern = "certifications", group = " Certifications" }, + + { message = ".*", group = " Other" }, +] \ No newline at end of file diff --git a/docs/pages/changelog.mdx b/docs/pages/changelog.mdx new file mode 100644 index 000000000..c7774200a --- /dev/null +++ b/docs/pages/changelog.mdx @@ -0,0 +1,5 @@ +# Changelog + +Release notes for the Security Frameworks, generated from [GitHub Releases](https://github.com/security-alliance/frameworks/releases). + +::changelog diff --git a/package.json b/package.json index 53caff303..df7af2a3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "workspace", - "version": "1.0.0", + "version": "0.0.1", "description": "Official repository to the Security Frameworks by SEAL. This repository contains the entire structure and contents of the frameworks. Feel free to suggest from new categories to grammar corrections. Collaboration is open to everyone. **This is a work in progress.**", "main": "index.js", "type": "module", diff --git a/utils/generate-folder-indexes.cjs b/utils/generate-folder-indexes.cjs index cebd83e34..50056e6b6 100644 --- a/utils/generate-folder-indexes.cjs +++ b/utils/generate-folder-indexes.cjs @@ -247,7 +247,7 @@ function loadSidebarConfig(branchName) { .replace(/^(?:import[^\n]*\n)+/, '') .replace(/export default defineConfig\(config\)\s*;?\s*$/, 'return defineConfig(config);') .replace(/\bas const\b/g, '') - .replace(/head\(\{[^}]*\}\s*:\s*\{[^}]*\}\)\s*\{[\s\S]*?\n \},/, '') + .replace(/head\([^)]*\)\s*\{[\s\S]*?\n \},/, '') .replace(/new Set<[^>]+>\(/g, 'new Set(') .replace(/function\s+(\w+)\(([^)]*)\)\s*:\s*[^\s{]+/g, (_, name, params) => { const cleaned = params.replace(/:\s*[^,)]+/g, ''); diff --git a/vocs.config.ts b/vocs.config.ts index 3f3842ff4..dd958e604 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -1,18 +1,21 @@ -import { createElement, Fragment } from 'react' -import { defineConfig } from 'vocs/config' +import { readFileSync } from 'node:fs' +import { Changelog, defineConfig } from 'vocs/config' -const MAIN_SITE_URL = 'https://frameworks.securityalliance.org' +function siteVersion(): string { + try { + const raw = readFileSync('VERSION', 'utf8').trim() + if (raw) return raw.startsWith('v') ? raw : `v${raw}` + } catch {} + return 'v0.0.0' +} const isMainBranch = process.env.CF_PAGES_BRANCH === 'main' const config = { srcDir: 'docs', renderStrategy: 'full-static' as const, - head({ path }: { path: string }) { - const cleanPath = path.replace(/\/index\.html$/, '').replace(/\.html$/, '').replace(/\/$/, '') - if (!isMainBranch && devOnlyLinks.has(cleanPath)) return createElement(Fragment) - const canonicalUrl = `${MAIN_SITE_URL}${cleanPath || '/'}` - return createElement('link', { rel: 'canonical', href: canonicalUrl }) + head(path: string) { const cleanPath = path.replace(/\/index\.html$/, '').replace(/\.html$/, '').replace(/\/$/, '') + return { canonical: `https://frameworks.securityalliance.org${cleanPath || '/'}` } }, banner: { content: 'This is a work in progress and not a release. We are looking for volunteers. See [Issues](https://github.com/security-alliance/frameworks/issues) and [Contribution](https://github.com/security-alliance/frameworks/blob/develop/docs/pages/contribute/contributing.mdx) to know how to collaborate.', @@ -28,6 +31,19 @@ const config = { iconUrl: 'https://frameworks-static.s3.us-east-2.amazonaws.com/images/logo/favicon.svg', ogImageUrl: 'https://frameworks-static.s3.us-east-2.amazonaws.com/images/logo/frameworks-full-cropped.png', checkDeadlinks: "warn" as const, + changelog: + typeof Changelog !== 'undefined' + ? Changelog.github({ repo: 'security-alliance/frameworks' }) + : undefined, + topNav: [ + { + text: siteVersion(), + items: [ + { text: 'Changelog', link: '/changelog' }, + { text: 'Contributing', link: '/contribute/contributing' }, + ], + }, + ], sidebar: [ { text: 'Introduction', @@ -701,6 +717,20 @@ function collectDevLinks(items: any[], parentIsDev = false): Set { const devOnlyLinks = collectDevLinks(config.sidebar) +if (!isMainBranch && devOnlyLinks.size > 0) { + const devRoutesLiteral = JSON.stringify([...devOnlyLinks]) + // @ts-expect-error - head is rebuilt from source so the dev-route list is + config.head = new Function( + 'path', + ` + const devOnlyLinks = new Set(${devRoutesLiteral}); + const cleanPath = path.replace(/\\/index\\.html$/, '').replace(/\\.html$/, '').replace(/\\/$/, ''); + if (devOnlyLinks.has(cleanPath)) return { canonical: false }; + return { canonical: 'https://frameworks.securityalliance.org' + (cleanPath || '/') }; + `, + ) +} + if (isMainBranch) { config.sidebar = filterDevItems(config.sidebar) }