FreeAndNil commented on PR #330:
URL: https://github.com/apache/logging-log4net/pull/330#issuecomment-6122184519
Implementation plan for #330, kept here for the record. Revised after the
merge so that it describes the change as it landed.
<details>
<summary>Plan</summary>
# PR #330: bind a release to one commit
## Context
f025 (LOW) of audit `da18b6fd`, `scripts/build-release.ps1`: the binaries
are built from the working
tree (`dotnet test`, line 31) while the source zip comes from the local
`master` ref
(`git archive ... master`, line 34). Nothing binds them. A dirty tree, a
different branch checked
out, or a local `master` that is not the built commit produces an officially
signed source zip
that does not correspond to the officially signed binaries, and reviewers
who read the source
learn nothing about the shipped binaries. Sub-claim, verified:
`sign-log4net-libraries.sh` has no
`nullglob` and no `set -e`, so a zero-artifact run iterates the literal glob
strings, sets
`DID_SOMETHING=1`, fails every `gpg` call and still exits 0. The
no-artifacts guard is dead code.
A second hole shows up while writing the verifier side: the hash and
signature loops are driven
from the files present, so an artifact deleted together with its `.sha512`
and `.asc` passes
verification. Binding the release to a commit therefore ships a signed
manifest that records the
commit **and** the artifact set, and both verifiers check both.
One commit.
## Branch
`Feature/330-release-from-one-commit`, off local `master`. #328 is open and
edits
`build-release.ps1` (`Write-HashAndSignature`) and `build-release.Tests.ps1`
(one `It`). The LF
fix for the `.sha512` files from #328 lands here, since the manifest needs
the same rule, and #328
is rebased onto this.
## The change
### `scripts/build-release.ps1`
Restructure around a `$Root` and an `$ArtifactDirectory` variable and four
small functions, so the
`pushd`/`popd` pairs go away:
- `Invoke-InDirectory`, `Push-Location` with the action in a `try`/`finally`
`Pop-Location`, so a
throw inside cannot leave the caller in the wrong directory. Used for the
archive, the zip and
the site build.
- `Get-ReleaseCommit`: in the root, `git status --porcelain`; any output
throws, listing the dirty
paths; otherwise returns `git rev-parse --verify HEAD`. Maintainer
decision: **clean tree and
archive from `HEAD`, no comparison against `origin/master`**, so no
network call in the release
script and a release can still be cut from a tag or a release branch.
- `Write-Manifest -Commit -Names`: writes `apache-log4net-$Version.manifest`
with one `commit=`
line and one `artifact=` line per artifact, the manifest itself included.
Written with
`-NoNewline` and the lines joined by `` `n ``: `Set-Content` writes
`[Environment]::NewLine`,
so on Windows every name would carry a trailing CR into the comparison in
`verify-release.sh`.
- `Remove-Directory`: `Test-Path` then `Remove-Item -Force -Recurse`, in
place of the
`-ErrorAction SilentlyContinue` deletes, which also swallowed a failed
delete and left a stale
artifact behind for whoever copies the directory to dist.
The flow: clean `build/`, verify the tree and capture the commit, `dotnet
test`, `git archive
--format=zip ... $CommitHash` in place of `master`, zip the binaries, rename
the two nupkgs, write
the manifest, then hash and sign every name in `$ArtifactNames` in one loop.
The artifact list is
declared once at the top: the two nupkgs, the source and binaries zips, the
two verifiers and the
manifest, **seven artifacts**. `Write-HashAndSignature` writes its `.sha512`
line with `-NoNewline`
and an explicit `` `n `` for the same reason as the manifest: the macOS
`sha512sum` reads a CR
from a Windows build as part of the file name.
The release candidate number becomes a parameter,
`[ValidateRange('Positive')] [int]$Rc = 1`, so
the tag is `rc/$Version-rc$Rc` rather than always `rc1`. `build-preview.ps1`
gets the same
validation on its `$Preview` number.
Note for the commit message, not a code change: `git archive --format=zip`
stores the commit id in
the zip's archive comment, which is what makes the verifier check below
independent of the
manifest.
### `scripts/verify-release.ps1` and `scripts/verify-release.sh`
Both verifiers gain the same two checks between the hash loop and the
signature loop, with the
PowerShell side refactored into `Get-Manifest`, `Assert-ArtifactSet`,
`Assert-SourceCommit` and
`Assert-Signature`:
- **The artifact set.** Exactly one `.manifest` among the artifacts, or fail
naming the count. Its
`artifact=` lines must be non-empty, every listed name must be present,
and every present
artifact must be listed, each failure naming the offending names.
- **The source commit.** Exactly one `commit=` line, exactly one
`*source*.zip`, and the recorded
commit must equal the zip's archive comment, read with
`ZipFile.OpenRead(...).Comment` in
PowerShell and `unzip -z -qq` in bash (`-qq`, or the `Archive:` banner
lands in the comparison).
Whitespace is trimmed on both sides.
No backward compatibility: each release ships the verifier built with it, so
a release without a
manifest is simply refused.
### `scripts/sign-log4net-libraries.sh`
`set -euo pipefail`, `shopt -s nullglob` with a one line comment, and quote
`"$f"` in both `gpg`
arguments. `verify-release.sh` already opens exactly this way, so this is
the file catching up
with its neighbour, not a new convention. The PowerShell twin does not have
the hole: `Get-Item`
errors on no match.
## Tests
`scripts/build-release.Tests.ps1` and `scripts/verify-release.Tests.ps1`,
Pester 5, discovered by
directory from `.github/workflows/build.yaml`. The scaffolding is
`scripts/FakeCommands.TestHelper.ps1`, which shadows native commands with
real shim files on a
prepended `PATH`, not Pester `Mock`.
**The helper gains a `git` case**, or every new test runs against an empty
`$CommitHash`:
- `git status --porcelain` produces no output by default and one modified
path when `FAKE_DIRTY`
is set, driven by a `-Dirty` switch on `Invoke-InScratchTree`. The
existing `FAKE_FAIL` switch
only makes a command exit 1, which is not the same thing.
- `git rev-parse --verify HEAD` echoes a fixed fake sha from `FAKE_SHA`,
exposed to the tests as
`$script:FakeCommitHash`, so `git archive` and the manifest get a real
value.
- The result gains `Lines`, the whole logged lines, for an assertion about
an argument rather than
about the command; `Calls` keeps the two-token projection.
Existing release tests change, because two `git` calls now precede `dotnet
test`: `'signs nothing
when the build fails'`, `'builds no site when signing fails'` and the full
run get `'git status',
'git rev-parse'` prepended, the full run has seven `gpg --armor` calls and
is renamed to "all seven
artifacts", and `'ships no artifact without a hash'` expects 7.
New release tests:
- `'writes every hash as one LF-terminated line'`: all seven `.sha512` files
match
`^[0-9a-f]{128} \*\./\S+\n\z`, read as bytes, so the CR cannot hide.
- `'removes the artifacts of an earlier run'`: a planted stale nupkg is gone
afterwards.
- `'builds nothing when the working tree is dirty'`: `-Dirty`, expect
`Calls` to be
`@('git status')` alone and a non-zero exit. This is the regression test
for the finding.
- `'archives the commit it built'`: the one `git archive` line ends in the
fake sha.
- `'records the commit and the artifact set in the manifest'`: one manifest,
the `commit=` line
holds the fake sha, the seven `artifact=` lines equal the files present,
and the file holds no
CR.
New verifier tests, with two fixture helpers: `Add-SourceArchive` writes a
zip whose archive
comment is a commit, as `git archive` produces, and `Add-Manifest` writes a
manifest that lists
itself, as the real one does, both with their `.sha512`. `Add-Artifact`
gains a `-Content` written
without a newline, so an empty content really is a zero byte file. The
cases: no manifest; an
artifact the manifest does not list; a listed artifact that is missing; a
manifest without a
commit line; a source archive whose comment is not the recorded commit. Each
asserts the message
of its own check.
`sign-log4net-libraries.sh` has no test and bash is not in the Pester
harness. Do not build one for
it; the change is three lines that mirror `verify-release.sh`, and the guard
it repairs is
reachable by hand (`cd` to an empty directory, run it, expect exit 2).
**Revert to fail**: undo the tree check in place, no `git stash`, and
confirm the dirty-tree test
fails.
## Changelog
`src/changelog/3.5.0/330-release-from-one-commit.xml`, `type="fixed"`,
`<issue id="330"
link="https://github.com/apache/logging-log4net/pull/330"/>`, both
attributes. Prose only, no
bullets. Name what it means for a verifier: the signed source zip and the
signed binaries now come
from one commit, a signed manifest ships beside them recording that commit
and the artifact set,
and the verification scripts check both against the zip archive comment and
the files present.
Close with `(audit da18b6fd-f025, fixed by @FreeAndNil)`.
`src/changelog/3.5.0/313-verify-release-keys-bypass.xml` is the precedent
that release-script
changes do get an entry.
## Agent instructions
Add a "Release scripts" section with the three rules this change
establishes: every file the
release writes gets LF on every platform, each release ships its own
verifier so the verifiers
carry no backward compatibility, and the scripts are tested through the fake
command helper on all
three CI operating systems, so the `$IsWindows` branch stays working and
byte-level assertions go
where all three see them.
## Verification
```
Invoke-Pester ./scripts -Output Detailed # all four test files, was
green before
dotnet build src/log4net.sln # 0 warnings, untouched by
this change
bash -n scripts/sign-log4net-libraries.sh # plus a by-hand run in an
empty directory, expect exit 2
```
The release script itself is never run for real here: the Pester harness
fakes `dotnet`, `git`,
`gpg`, `zip` and `mvnw`, so nothing is built, signed, tagged or pushed. CI
runs the same suite on
macOS, Ubuntu and Windows, which is where the LF assertions earn their keep.
## House rules
The maintainer commits. Leave the work unstaged, write the commit message to
`.git/plans/330/commit.txt` and stop. No `git add`, no `git commit`, no
attribution trailer.
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]