mirror of
https://github.com/gopasspw/gopass.git
synced 2026-09-29 11:12:40 +02:00
* docs(adr): resolve the A-13 collision, zero-pad numbers and add an index
Two records carried the number A-13:
docs/adr/A-13-expired-gpg-key-handling.md
docs/adr/A-13-screenshot-build-tag.md
An ADR number is a stable identifier, so a duplicate makes every citation
ambiguous. A-13-expired-gpg-key-handling.md keeps the number: it is cited
from docs/commands/recipients.md, docs/usecases/team-workflows.md and
docs/adr/A-14-team-workflows.md. The screenshot record has no inbound
citations and is renumbered to A-15.
Zero-pad A-3 through A-9 to A-03 through A-09 so the directory sorts
correctly now that the set has passed ten entries. None of these has an
inbound citation from another document; the single reference in
internal/backend/storage/fs/rcs.go is updated in this commit.
Add docs/adr/README.md as the index, recording the naming rules, the status
and authoring date of every record, and three facts that are otherwise only
discoverable from git history:
- A-01 and A-02 are cited from the CHANGELOG unreleased section but no file
was ever written for either; the numbers stay reserved.
- The A-13 collision and which record was renumbered.
- SECURITY_AUDIT_REPORT.md and CODE_QUALITY_REPORT.md, cited as the Source
of A-03 through A-10, were removed in 77894053 and are not in the tree.
No record content is changed apart from the H1 lines, which must match the
file names.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: add docs/conventions.md and adopt Conventional Commits
CONTRIBUTING.md required a bracketed [TAG] prefix on every commit subject.
That has not matched practice for some time: of the 344 commit subjects
since 2025-01-01, 225 are Conventional Commits and 67 use a [TAG]. The
CHANGELOG unreleased section uses a third set including [SECURITY] and
[PKG-BREAK], neither of which CONTRIBUTING.md lists.
Replace the [TAG] rule with Conventional Commits and add
docs/conventions.md as the single normative reference for:
- commit types (a closed list) and the scopes derived from the package
layout, including the five values that appear as types in the history but
are scopes: otp, age, bug, fscopy, openbsd;
- the distinction between a CLI break, which uses "!" and a
BREAKING CHANGE: footer and forces a major release, and a break confined
to pkg/gopass, which uses a PKG-BREAK: footer and does not (ADR A-12);
- Semantic Versioning, and which surfaces it does and does not cover;
- branch and tag names, including the release/ and prep/ prefixes owned by
the release automation;
- file naming for ADRs, documentation and Go sources.
The Developer Certificate of Origin requirement is unchanged. Conventional
Commits governs the subject line and the DCO adds a trailer, so the two are
independent.
Also correct the API Stability section of ARCHITECTURE.md, which still
described pkg/gopass/doc.go as carrying "an explicit instability warning"
and instructed consumers to "treat any pkg/ type or function change as
potentially breaking". Both statements predate ADR A-12: doc.go now
declares the package best-effort stable and permits additive changes in any
release. The section also referred to issue #3414 as an open decision; that
decision is recorded in A-12 with status accepted.
Extend the folder list in AGENTS.md with the five pkg/ directories it does
not mention (otp, passkey, pinentry/cli, protect, qrcon), using each
package's own doc comment as the description.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(changelog): migrate to Keep a Changelog 1.1.0
CHANGELOG.md used a bespoke format with three overlapping entry conventions:
the bracketed [TAG] prefixes CONTRIBUTING.md mandated, the Conventional
Commit subjects that have been in use since 2025, and a third set in the
unreleased section including [SECURITY] and [PKG-BREAK] that appeared in
neither. Release 1.16.0 contains the first two mixed together.
Adopt Keep a Changelog 1.1.0 and generate the entries from commit subjects.
New package helpers/commitmsg
Parse and classify commit subjects. It is the single source of truth for
both the changelog generator and any future commit linting, so the two
cannot disagree about what a valid subject is. It implements the closed
type list from docs/conventions.md, the Changelog-Section:, PKG-BREAK:,
BREAKING CHANGE: and RELEASE_NOTES= footers, and the legacy [TAG] and
bracketed-type forms so a release spanning the transition still classifies
its older commits.
Classify returns a disposition rather than a bare boolean, distinguishing
a deliberate omission -- a dependency bump, a CI change -- from a subject
it could not recognise. That distinction matters: over the 137 commits
since v1.16.1 it classifies 47, omits 60 and cannot recognise 30. Among
the 30 are real user-facing changes such as "otp: hide --snip flag when
built with noscreenshot tag", which uses a scope where a type belongs.
The release helper now prints those subjects instead of dropping them
silently.
helpers/release: fix the section ordering defect
writeChangelog inserted the new release before the first "## " heading.
Once an unreleased section existed, that heading was the unreleased one,
so the release landed above it and the hand-written entries below were
orphaned -- never published, and silently carried into every subsequent
release. The 30 entries currently under "## Next" are in exactly that
state.
The new implementation in helpers/release/changelog.go parses the file
into header, unreleased block, released body and link references; merges
the hand-written entries with the generated ones and de-duplicates;
renders the release with only its non-empty subsections, in Keep a
Changelog order; leaves an empty Unreleased section behind; and rewrites
the two link references a release changes.
helpers/changelog: skip the unreleased section
The extractor printed everything between the first and second "## "
heading. After the migration the first heading is an empty
"## [Unreleased]", which would have produced empty GitHub release notes.
It now extracts the first *versioned* section.
CHANGELOG.md data migration
The 30 entries under "## Next" move into "## [Unreleased]" and are
distributed by their existing tags. Three are placed by meaning rather
than by tag, because Keep a Changelog has sections their tags do not:
the two [UX] entries that remove a CLI alias go to Removed, and the [UX]
entry about the GOPASS_AUTOSYNC_INTERVAL deprecation goes to Deprecated.
The bracketed prefixes are dropped; the trailing audit identifiers such
as (A-1) and (B-8) are kept, since they are the only trace back to the
audit that produced those entries.
All 86 released headings become "## [X.Y.Z] - YYYY-MM-DD" with their
entry text untouched. The two headings that carried no date, 1.10.0 and
1.10.1, take theirs from their git tags. A link reference block is
appended; every one of the 86 versions has a matching tag.
The bullet count is unchanged at 837.
Verified end to end: "go run ./helpers/changelog" against the migrated file
extracts the 1.16.1 section and does not capture [Unreleased].
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): correct the swapped Next/Prev labels and fold duplicate entries
Two follow-ups found while verifying the changelog migration end to end.
The "Version overview" block printed by every release run labels its third
and fourth values "Next version flag" and "Prev version flag", but passes
them as prevVerFlag, nextVerFlag. Running
go run helpers/release/main.go --dry-run v1.17.0
therefore reported "Next version flag: ''" and "Prev version flag: '1.17.0'"
for a flag that sets the next version. The values are swapped to match the
labels. Output only; no behaviour depends on it.
Changelog entries are now de-duplicated case-insensitively. A hand-written
unreleased entry and the subject of the commit that implemented it commonly
differ only in their first letter, for example "Add gopass doctor diagnostic
command (I-4)" against "add gopass doctor diagnostic command (I-4)". This
cannot catch two genuinely different wordings of the same change; those
still need a human pass before the release.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(adr): align A-12 with the Keep a Changelog format
Three statements in A-12 no longer hold once CHANGELOG.md follows Keep a
Changelog:
- The entry goes in "## [Unreleased]", not "## Next".
- "The existing helpers/changelog generator reads CHANGELOG.md verbatim; no
changes to that tool are needed" is false. The extractor had to learn to
skip the unreleased section, and the release helper now classifies commits
rather than copying subjects.
- "[SECURITY], [BUGFIX] and [FEATURE]" are no longer tags. They are the
Security, Fixed and Added subsections.
State the mechanism instead: a PKG-BREAK: commit footer, which
helpers/commitmsg reads and helpers/release renders as a [PKG-BREAK]-prefixed
bullet inside the appropriate subsection. [PKG-BREAK] survives as a bullet
prefix because it qualifies an entry rather than categorising it, which is
exactly what a Keep a Changelog subsection cannot express.
Writing the prefix by hand into "## [Unreleased]" still works; the release
helper preserves it.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
304 lines
8.9 KiB
Go
304 lines
8.9 KiB
Go
// Copyright 2026 The gopass Authors. All rights reserved.
|
|
// Use of this source code is governed by the MIT license,
|
|
// that can be found in the LICENSE file.
|
|
|
|
// Package commitmsg parses commit subjects and classifies them into Keep a
|
|
// Changelog sections. It is the single source of truth shared by the changelog
|
|
// generator in helpers/release and by any commit message linting, so the two
|
|
// cannot disagree about what a valid subject looks like.
|
|
//
|
|
// The conventions it implements are specified in docs/conventions.md.
|
|
package commitmsg
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Section is a Keep a Changelog 1.1.0 subsection.
|
|
type Section string
|
|
|
|
// The six sections defined by Keep a Changelog 1.1.0.
|
|
const (
|
|
Added Section = "Added"
|
|
Changed Section = "Changed"
|
|
Deprecated Section = "Deprecated"
|
|
Removed Section = "Removed"
|
|
Fixed Section = "Fixed"
|
|
Security Section = "Security"
|
|
|
|
// Omit marks a commit type that produces no changelog entry.
|
|
Omit Section = ""
|
|
)
|
|
|
|
// Sections lists the sections in the order Keep a Changelog prescribes.
|
|
// Render them in this order and skip the empty ones.
|
|
var Sections = []Section{Added, Changed, Deprecated, Removed, Fixed, Security}
|
|
|
|
// Types maps every accepted Conventional Commit type to its changelog section.
|
|
// The list is closed: a subject using any other type is not a valid commit
|
|
// message. Types mapping to Omit produce no changelog entry.
|
|
var Types = map[string]Section{
|
|
"build": Omit,
|
|
"chore": Omit,
|
|
"ci": Omit,
|
|
"deps": Changed,
|
|
"docs": Omit,
|
|
"feat": Added,
|
|
"fix": Fixed,
|
|
"perf": Changed,
|
|
"refactor": Changed,
|
|
"revert": Changed,
|
|
"security": Security,
|
|
"test": Omit,
|
|
}
|
|
|
|
// legacyTags maps the bracketed tags used before the move to Conventional
|
|
// Commits. They are recognised so that a release spanning the transition
|
|
// classifies its older commits correctly.
|
|
var legacyTags = map[string]Section{
|
|
"BREAKING": Changed,
|
|
"BUGFIX": Fixed,
|
|
"CLEANUP": Changed,
|
|
"DEPRECATION": Deprecated,
|
|
"DOCUMENTATION": Omit,
|
|
"ENHANCEMENT": Changed,
|
|
"FEATURE": Added,
|
|
"PKG-BREAK": Changed,
|
|
"SECURITY": Security,
|
|
"TESTING": Omit,
|
|
"UX": Changed,
|
|
}
|
|
|
|
// Prefixes applied to an entry's text.
|
|
const (
|
|
breakingPrefix = "**BREAKING** "
|
|
pkgBreakPrefix = "[PKG-BREAK] "
|
|
revertPrefix = "Revert: "
|
|
)
|
|
|
|
var (
|
|
// conventionalRE matches a Conventional Commits 1.0.0 subject.
|
|
// Groups: type, scope, breaking marker, description.
|
|
conventionalRE = regexp.MustCompile(`^([a-z]+)(?:\(([^)]*)\))?(!)?: (.+)$`)
|
|
|
|
// legacyRE matches the bracketed subject form used before the move to
|
|
// Conventional Commits, for example "[BUGFIX] reorg: list all secrets".
|
|
legacyRE = regexp.MustCompile(`^\[([A-Za-z-]+)\]\s+(.+)$`)
|
|
|
|
// sectionFooterRE matches the Changelog-Section: footer, which overrides
|
|
// the type-to-section mapping. It is the only way to reach the Removed and
|
|
// Deprecated sections, which have no corresponding commit type.
|
|
sectionFooterRE = regexp.MustCompile(`(?m)^Changelog-Section:\s*(\w+)\s*$`)
|
|
|
|
// pkgBreakFooterRE matches the PKG-BREAK: footer mandated by ADR A-12 for a
|
|
// breaking change confined to the pkg/gopass Go module.
|
|
pkgBreakFooterRE = regexp.MustCompile(`(?m)^PKG-BREAK:\s*(.+)$`)
|
|
|
|
// breakingFooterRE matches the Conventional Commits footer that marks a
|
|
// break in the CLI.
|
|
breakingFooterRE = regexp.MustCompile(`(?m)^BREAKING[ -]CHANGE:\s*(.+)$`)
|
|
|
|
// releaseNotesRE matches the pre-existing RELEASE_NOTES= override. A value
|
|
// of "n/a" suppresses the entry entirely.
|
|
releaseNotesRE = regexp.MustCompile(`(?m)^RELEASE_NOTES=(.*)$`)
|
|
)
|
|
|
|
// Commit is a parsed commit subject.
|
|
type Commit struct {
|
|
// Type is the Conventional Commit type, or the empty string for a legacy
|
|
// bracketed subject.
|
|
Type string
|
|
// Scope is the optional parenthesised scope. It is empty when absent.
|
|
Scope string
|
|
// Breaking reports whether the subject carried the "!" marker.
|
|
Breaking bool
|
|
// Description is the subject with the type, scope and marker removed. For a
|
|
// legacy subject it is everything after the bracketed tag.
|
|
Description string
|
|
// Legacy reports whether the subject used the bracketed form.
|
|
Legacy bool
|
|
// Section is the section the type maps to, before any footer override.
|
|
Section Section
|
|
}
|
|
|
|
// Entry is one rendered changelog bullet.
|
|
type Entry struct {
|
|
Section Section
|
|
Text string
|
|
}
|
|
|
|
// Disposition is what Classify decided to do with a commit.
|
|
type Disposition int
|
|
|
|
const (
|
|
// Include means the commit produced a changelog entry.
|
|
Include Disposition = iota
|
|
// Omitted means the commit was deliberately excluded: its type maps to
|
|
// Omit, or its body carried RELEASE_NOTES=n/a. This is the expected
|
|
// outcome for dependency bumps, CI changes and documentation.
|
|
Omitted
|
|
// Unrecognised means the subject is not a valid commit message, so no
|
|
// section could be chosen. Callers must surface these rather than drop
|
|
// them silently: a real user-facing change written as "otp: hide --snip
|
|
// flag" -- a scope used as a type -- would otherwise vanish from the
|
|
// changelog without a trace.
|
|
Unrecognised
|
|
)
|
|
|
|
// Parse splits a commit subject into its parts. It reports false when the
|
|
// subject is neither a Conventional Commit with a known type nor a legacy
|
|
// bracketed subject with a known tag.
|
|
func Parse(subject string) (Commit, bool) {
|
|
subject = strings.TrimSpace(subject)
|
|
|
|
if m := conventionalRE.FindStringSubmatch(subject); m != nil {
|
|
sec, ok := Types[m[1]]
|
|
if !ok {
|
|
return Commit{}, false
|
|
}
|
|
|
|
return Commit{
|
|
Type: m[1],
|
|
Scope: m[2],
|
|
Breaking: m[3] == "!",
|
|
Description: m[4],
|
|
Section: sec,
|
|
}, true
|
|
}
|
|
|
|
if m := legacyRE.FindStringSubmatch(subject); m != nil {
|
|
if sec, ok := legacyTags[strings.ToUpper(m[1])]; ok {
|
|
return Commit{
|
|
Description: m[2],
|
|
Legacy: true,
|
|
Section: sec,
|
|
Breaking: strings.EqualFold(m[1], "BREAKING"),
|
|
}, true
|
|
}
|
|
|
|
// The history also contains a hybrid form that puts a Conventional
|
|
// Commit type in brackets, for example "[fix] Support HW Age
|
|
// identities" and "[chore] Update gopasspw/clipboard". Accept it so a
|
|
// release spanning the transition does not lose those entries.
|
|
if sec, ok := Types[strings.ToLower(m[1])]; ok {
|
|
return Commit{
|
|
Type: strings.ToLower(m[1]),
|
|
Description: m[2],
|
|
Legacy: true,
|
|
Section: sec,
|
|
}, true
|
|
}
|
|
|
|
return Commit{}, false
|
|
}
|
|
|
|
return Commit{}, false
|
|
}
|
|
|
|
// Text renders the changelog bullet text for a parsed commit: the description,
|
|
// prefixed with the scope when there is one. The type is dropped because the
|
|
// section heading already carries it -- "fix(age): expand ~/" under a "Fixed"
|
|
// heading renders as "age: expand ~/".
|
|
func (c Commit) Text() string {
|
|
if c.Scope == "" {
|
|
return c.Description
|
|
}
|
|
|
|
return c.Scope + ": " + c.Description
|
|
}
|
|
|
|
// Classify turns a commit subject and body into a changelog entry, and reports
|
|
// why it did so.
|
|
//
|
|
// Precedence, highest first: RELEASE_NOTES=n/a suppresses everything;
|
|
// RELEASE_NOTES=<text> replaces the text; Changelog-Section: replaces the
|
|
// section; PKG-BREAK: forces an entry and adds a prefix; "!" or
|
|
// BREAKING CHANGE: adds a prefix.
|
|
func Classify(subject, body string) (Entry, Disposition) {
|
|
notes, hasNotes := releaseNotes(body)
|
|
if hasNotes && notes == "" {
|
|
// RELEASE_NOTES=n/a: an explicit request to stay out of the changelog.
|
|
return Entry{}, Omitted
|
|
}
|
|
|
|
c, ok := Parse(subject)
|
|
if !ok && !hasNotes {
|
|
return Entry{}, Unrecognised
|
|
}
|
|
|
|
sec := c.Section
|
|
text := c.Text()
|
|
|
|
if hasNotes {
|
|
text = notes
|
|
if !ok {
|
|
// An unparseable subject with an explicit release note still
|
|
// deserves an entry; Changed is the least surprising home for it.
|
|
sec = Changed
|
|
}
|
|
}
|
|
|
|
pkgBreak := pkgBreakFooterRE.FindStringSubmatch(body)
|
|
if pkgBreak != nil && sec == Omit {
|
|
// A pkg/gopass break must always be visible, whatever the type says.
|
|
sec = Changed
|
|
}
|
|
|
|
if m := sectionFooterRE.FindStringSubmatch(body); m != nil {
|
|
if s, valid := parseSection(m[1]); valid {
|
|
sec = s
|
|
}
|
|
}
|
|
|
|
if sec == Omit {
|
|
return Entry{}, Omitted
|
|
}
|
|
|
|
if c.Breaking || breakingFooterRE.MatchString(body) {
|
|
text = breakingPrefix + text
|
|
}
|
|
|
|
if pkgBreak != nil {
|
|
text = pkgBreakPrefix + text
|
|
}
|
|
|
|
if c.Type == "revert" {
|
|
text = revertPrefix + text
|
|
}
|
|
|
|
return Entry{Section: sec, Text: text}, Include
|
|
}
|
|
|
|
// parseSection resolves a Changelog-Section: footer value, case-insensitively.
|
|
func parseSection(in string) (Section, bool) {
|
|
for _, s := range Sections {
|
|
if strings.EqualFold(in, string(s)) {
|
|
return s, true
|
|
}
|
|
}
|
|
|
|
return Omit, false
|
|
}
|
|
|
|
// releaseNotes extracts the RELEASE_NOTES= override from a commit body. It
|
|
// returns an empty string with ok set when the value is "n/a", which means the
|
|
// commit is deliberately excluded from the changelog.
|
|
func releaseNotes(body string) (string, bool) {
|
|
m := releaseNotesRE.FindStringSubmatch(body)
|
|
if m == nil {
|
|
return "", false
|
|
}
|
|
|
|
val := strings.TrimSpace(m[1])
|
|
if strings.EqualFold(val, "n/a") {
|
|
return "", true
|
|
}
|
|
|
|
if val == "" {
|
|
return "", false
|
|
}
|
|
|
|
return val, true
|
|
}
|