Simplify source releases with one guarded publication command
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# Advisory validation after source publication, never a release-completion gate.
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
@@ -5,29 +6,4 @@ steps:
|
||||
- name: validate-release
|
||||
image: golang:1.25.5
|
||||
commands:
|
||||
- |
|
||||
set -eu
|
||||
|
||||
version="$CI_COMMIT_TAG"
|
||||
release_note="docs/releases/$version.md"
|
||||
|
||||
if ! printf '%s\n' "$version" | grep -E -x 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' >/dev/null; then
|
||||
printf '%s\n' "invalid release tag: $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -s "$release_note" ]; then
|
||||
printf '%s\n' "missing release note: $release_note" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -F -x "# Notarius $version" "$release_note" >/dev/null; then
|
||||
printf '%s\n' "release note heading does not match $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
for heading in '## Summary' '## Compatibility' '## Upgrade' '## Changes'; do
|
||||
if ! grep -F -x "$heading" "$release_note" >/dev/null; then
|
||||
printf '%s\n' "release note is missing heading: $heading" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
./scripts/check-release-source.sh "$version"
|
||||
- ./scripts/check-release-source.sh "$CI_COMMIT_TAG"
|
||||
|
||||
@@ -28,7 +28,7 @@ implementing changes. It owns implementation discipline and local commit rules.
|
||||
| LLM clients, prompts, schemas, profiles, or scheduling | [LLM Runtime](internal/llm.md) | It documents the transport boundary and PromptKit integration. |
|
||||
| Output, cache, resume, or debug artifacts | [Run State Internals](internal/state.md), [Operations](operations.md), and [Configuration](config.md) | These separate implementation details, operator behavior, and configuration contracts. |
|
||||
| External input formats, artifact schemas, or durable output files | [Integration Contracts](integrations/) | Integration documents define external and durable data contracts. |
|
||||
| Release preparation, tagging, publication, or verification | [Source Releases](release.md) and [Documentation Policy](policy/documentation.md) | The release procedure owns maintainer guards and immutable-tag recovery; the policy assigns release-note ownership. |
|
||||
| Release preparation, tagging, publication, or optional diagnostics | [Release Policy](policy/release.md) and [Source Releases](release.md) | Policy owns release requirements and completion; the runbook supplies the publication command and recovery procedure. |
|
||||
| Proposed or unimplemented behavior | [Roadmap](roadmap/) | Future work belongs only in roadmap documentation until implemented. |
|
||||
|
||||
For an existing subsystem, also inspect its focused tests and the package-local
|
||||
|
||||
@@ -357,8 +357,9 @@ best-effort development and compilation environment, while Windows is
|
||||
unsupported. Notarius distributes source releases only: an immutable source
|
||||
tag and its checked-in release note identify a release. The project does not
|
||||
publish executable binaries, archives, installers, container images,
|
||||
checksums, signatures, or package-manager entries. Maintainer release commands
|
||||
and tag guards belong to [Source Releases](../release.md).
|
||||
checksums, signatures, or package-manager entries. [Release Policy](release.md)
|
||||
owns publication requirements and completion; maintainer commands and guards
|
||||
belong to [Source Releases](../release.md).
|
||||
|
||||
## Architectural Non-Goals
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ secret values.
|
||||
| CLI contract | `docs/cli.md` | Commands, arguments, flags, invocation semantics, and exit codes. | End-to-end operating procedures, configuration field definitions, runtime filesystem layout, module implementation details. |
|
||||
| Configuration contract | `docs/config.md` | Discovery and precedence, file schema, fields, defaults, environment overrides, validation rules, and user-selectable module or validator keys. | Complete example files, CLI syntax, runtime state lifecycle, module implementation details. |
|
||||
| Operations | `docs/operations.md` | Runtime workflows, physical filesystem and state layout, output, cache, and debug handling, resume, cleanup, permissions, recovery, and operational limits. | CLI flag syntax, configuration field definitions, logical output schemas, implementation mechanics. |
|
||||
| Source release procedure | `docs/release.md` | Maintainer release selection, candidate validation, tagging, publication guards, verification, and immutable-tag recovery. | Product installation summary, CLI version semantics, historical release summaries, CI implementation detail. |
|
||||
| Release policy | `docs/policy/release.md` | Versioning, compatibility, integrity, immutability, proportionate gate selection, publication controls, and release completion. | Concrete commands, product installation, historical notes, and CI implementation detail. |
|
||||
| Repository-specific release procedure | `docs/release.md` | Prerequisites, candidate preparation, publication command, automated guards, optional diagnostics, and failure recovery. | General release requirements, product installation guidance, historical summaries, and current compatibility contracts. |
|
||||
| Release-note history | `docs/releases/` | One checked-in historical summary for each source release made under the procedure. The note at the immutable tag is that release's record. | Current commands, behavior, contracts, and compatibility definitions. |
|
||||
| Public HTTP contract, if introduced | `docs/api.md` | Routes, authentication, media types, request and response schemas, status codes, pagination, caching, idempotency, rate limits, and HTTP retry semantics. | Client walkthroughs, upstream or downstream integration internals, implementation detail. |
|
||||
| Consumer guidance | `docs/consumers/` | Task-oriented subprocess use, minimal client examples, consumer responsibilities, and guidance for any future public package or API. | Wire semantics, external protocol contracts, internal implementation detail. |
|
||||
@@ -125,11 +126,13 @@ other contracts.
|
||||
|
||||
### Releases
|
||||
|
||||
`docs/release.md` owns the source-release procedure. Release notes are
|
||||
The [release policy](release.md) owns requirements and invariants; the
|
||||
[repository procedure](../release.md) owns the commands that satisfy them.
|
||||
Release notes are
|
||||
historical summaries, not current-state contract owners: the checked-in note at
|
||||
an immutable tag records that release, while current canonical documentation
|
||||
must change with the behavior it describes. Do not use a release note to defer
|
||||
or replace current documentation updates.
|
||||
must change with the behavior it describes. Do not use a release procedure or
|
||||
note to weaken policy, redefine current contracts, or defer documentation updates.
|
||||
|
||||
### Contracts And Implementation
|
||||
|
||||
|
||||
204
docs/policy/release.md
Normal file
204
docs/policy/release.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Release Policy
|
||||
|
||||
**Base Policy Version:** 1.1
|
||||
|
||||
**Last Updated**: 2026-09-25
|
||||
|
||||
**Upstream Source:** `policy` commit `45591a79004c6811950f14dca554f6ff69b7f58e`.
|
||||
|
||||
Notarius adaptations: source-only delivery, lightweight tags, branch-before-tag
|
||||
publication, no prereleases, and the justified native command check below.
|
||||
|
||||
## Purpose
|
||||
|
||||
This policy defines a proportionate workflow for validating and publishing an
|
||||
intended source revision under a fresh version. Releases should require little
|
||||
routine intervention, avoid repeated work, and preserve published contents.
|
||||
|
||||
The repository-specific commands belong in `docs/release.md`. Historical release
|
||||
summaries belong in `docs/releases/`. The procedure must agree with the adopted
|
||||
policy and link to it rather than introduce additional release requirements.
|
||||
|
||||
## Release Model And Completion
|
||||
|
||||
A source release identifies one exact committed source revision through an
|
||||
immutable version tag and a version-matched, checked-in release note. The
|
||||
primary release branch is `main` and the authoritative remote is `origin`.
|
||||
Tags are lightweight. Publish the candidate to `main` before pushing its tag.
|
||||
|
||||
A successful push of the intended tag, with its source commit available on the
|
||||
authoritative primary branch, completes the release. Notarius uses
|
||||
branch-before-tag publication; it does not require an atomic push.
|
||||
|
||||
Release commands must finish at successful publication. They must not wait for
|
||||
or poll CI, require successful CI status, inspect downloadable artifacts, or
|
||||
check consumer installation or Go module availability as completion gates.
|
||||
A failed or ambiguous push requires resolving the authoritative remote state
|
||||
before declaring success or retrying; successful pushes do not require a
|
||||
separate remote read-back gate.
|
||||
|
||||
Notarius publishes source only: no binaries, archives, images, packages,
|
||||
checksums, signatures, or Gitea release objects. Tag CI may provide advisory
|
||||
validation but publishes no deliverables and is not a completion gate.
|
||||
|
||||
## Versioning And Compatibility
|
||||
|
||||
Stable releases use Semantic Versioning in the form `vMAJOR.MINOR.PATCH`, with
|
||||
no leading zeroes. Notarius does not support prereleases.
|
||||
|
||||
The selected version must be unused locally and on the authoritative remote.
|
||||
Do not require queries to every configured remote or publication service.
|
||||
A version known to have already been published must not be reused, even if the
|
||||
normal guards do not inspect that channel. Adding a distribution channel
|
||||
requires a deliberate update to the policy and procedure where needed.
|
||||
|
||||
For releases at or after `v1.0.0`:
|
||||
|
||||
- a major release may make intentional incompatible changes;
|
||||
- a minor release may add backward-compatible behavior; and
|
||||
- a patch release may make backward-compatible fixes.
|
||||
|
||||
Before `v1.0.0`, a minor release may change a documented public, operational,
|
||||
configuration, data, integration, or compatibility contract when the release
|
||||
note explains the impact and required consumer action. A patch release must
|
||||
not intentionally break those contracts within its minor line.
|
||||
|
||||
Additional compatibility promises and deliberate exceptions belong in the
|
||||
adopted policy or canonical contract documentation. Release notes explain
|
||||
changes; they do not redefine current compatibility contracts.
|
||||
|
||||
## Release Documentation And Preparation
|
||||
|
||||
Every new release must include `docs/releases/<version>.md` in the tagged
|
||||
source tree. Use the complete version tag as the filename and begin with a
|
||||
title containing the product name and that exact version.
|
||||
|
||||
Include a short summary and any relevant compatibility impact or upgrade
|
||||
instructions. Keep the note proportionate to the change. Fixed sections and
|
||||
explicit no-action boilerplate are not required. Existing historical notes
|
||||
need not be rewritten to adopt this format.
|
||||
|
||||
Keep exact current contracts in their canonical documents and update affected
|
||||
documentation with the behavior change. Link to those documents from the note
|
||||
where useful. A release note must not substitute for current documentation.
|
||||
Do not create retrospective notes for the pre-procedure v0.1.0, v0.2.0, or
|
||||
v0.3.0 tags.
|
||||
|
||||
Version selection, release-note content, and retirement of completed roadmaps
|
||||
are preparation responsibilities, not separate human approval gates. Preserve
|
||||
planned and deferred work and repair affected links when retiring roadmaps.
|
||||
Ordinary change review covers unintended files, credentials, and private data;
|
||||
do not repeat it as a release-specific sign-off. Ignored local files are not
|
||||
prohibited merely because they exist: they are not included in the tagged source.
|
||||
|
||||
## Candidate Validation
|
||||
|
||||
Provide one publication command that validates the final committed candidate,
|
||||
checks publication state, and publishes the intended refs. A validation-only
|
||||
mode or command may be provided for diagnosis, but must not be a required step
|
||||
before publication repeats the same checks.
|
||||
|
||||
Run the complete deterministic, offline default test suite once for the final
|
||||
candidate, following the [testing policy](testing.md). For repositories that
|
||||
publish builds, verify the required build targets. Select those targets for
|
||||
the application's actual support commitments; native testing on each platform
|
||||
and access to particular hardware are not default release prerequisites.
|
||||
|
||||
Additional gates must address a concrete risk that the existing checks do not
|
||||
adequately cover. Do not automatically require linting, race testing, module
|
||||
hygiene audits, documentation audits, credential scans, example validation, or
|
||||
expanded platform matrices solely because they are possible. Retain such
|
||||
checks when their distinct protection justifies their cost. A one-time migration
|
||||
check must not silently become a permanent release requirement.
|
||||
|
||||
Release tests must not require real credentials, mutable application services,
|
||||
paid APIs, provider probes, or inference calls. The procedure may describe dependency
|
||||
preparation separately from offline validation.
|
||||
|
||||
Avoid duplicate validation across release phases. Successful checks for the
|
||||
same final candidate need not be repeated solely to satisfy another phase;
|
||||
any reuse must clearly apply to that exact candidate. Persistent validation
|
||||
caches are not required. If the candidate changes, validate the new candidate.
|
||||
Cheap publication-state checks may be repeated immediately before pushing
|
||||
because local or remote state may have changed during validation.
|
||||
|
||||
Automate mechanical guards. Human review gates are strongly disfavored: add one
|
||||
only through an explicit project-specific policy decision explaining the
|
||||
concrete risk and why ordinary change review or automation is insufficient.
|
||||
Do not add routine confirmations for checks the command can perform itself.
|
||||
|
||||
### Notarius Gate Selection
|
||||
|
||||
Run the default offline Go suite once and build the native `cmd/notarius` with
|
||||
release version injection, checking its `--version` output. The command check
|
||||
protects the actual executable entry point and linker-supplied version, which
|
||||
package tests alone do not verify. Build products are temporary, not published.
|
||||
Use `GOWORK=off` so a sibling checkout cannot substitute for declared dependencies.
|
||||
|
||||
Race tests, standalone vet, formatting/whitespace checks, module tidy audits,
|
||||
standalone example validation, and Linux/Darwin cross-builds remain useful
|
||||
change-specific development checks, not mandatory release gates. Default tests
|
||||
already cover maintained examples. Source-only publication does not require a
|
||||
binary distribution matrix. No new platform or hardware prerequisites are added.
|
||||
|
||||
## Publication Guards
|
||||
|
||||
Automate the checks needed to publish the intended source under a fresh version:
|
||||
|
||||
- the version has the adopted format and is unused locally and on the
|
||||
authoritative remote;
|
||||
- the worktree and index are clean on the primary release branch;
|
||||
- the source being published is the exact candidate that passed validation,
|
||||
including its matching release note;
|
||||
- the branch push advances authoritative `main` to the candidate without
|
||||
discarding remote history, before the tag is published; and
|
||||
- the tag identifies the validated candidate and only the intended refs are
|
||||
published.
|
||||
|
||||
Fetch the authoritative branch and tag state as needed for these checks.
|
||||
Do not force branch or tag updates to bypass a failed guard. Publish explicit
|
||||
refs rather than all local tags. Do not require CI success, publication-service
|
||||
availability checks, or a separate manual confirmation for each guard.
|
||||
|
||||
## Immutability And Failure Recovery
|
||||
|
||||
Published version tags and published contents are immutable. Do not move,
|
||||
overwrite, recreate, or delete a published tag, or replace a published artifact,
|
||||
checksum, signature, provenance statement, or release note under that version.
|
||||
The checked-in note at the immutable tag is that release's historical record.
|
||||
|
||||
If validation fails before publication, correct and commit the candidate and
|
||||
rerun the publication command. If a push fails or its outcome is ambiguous,
|
||||
inspect the authoritative remote before retrying. Remove an unpublished local
|
||||
tag only after confirming it was never published or used by a release channel.
|
||||
An unavailable remote leaves publication status unknown; do not overwrite a tag
|
||||
to resolve uncertainty.
|
||||
|
||||
Transient network, authentication, CI, or consumer-availability failures do not
|
||||
by themselves require a new version. Advisory CI or optional installation checks
|
||||
may be retried against the same immutable tag. Notarius has no asynchronous
|
||||
artifact delivery to recover.
|
||||
|
||||
A defect requiring changes to published source or contents requires a corrected
|
||||
commit, a new semantically appropriate version, a matching release note, and
|
||||
the release procedure again. Leave the original release intact. A channel's
|
||||
yank, deprecation, or advisory mechanism may warn consumers away from a defective
|
||||
release without replacing its contents or reusing its version.
|
||||
|
||||
## Repository Release Procedure
|
||||
|
||||
Maintain `docs/release.md` as a short, executable maintainer runbook. It must
|
||||
identify:
|
||||
|
||||
1. prerequisites, primary branch, and authoritative remote;
|
||||
2. version and release-note preparation;
|
||||
3. one publication command with its selected local checks and automated guards;
|
||||
4. declared deliverables, required build targets, and automation responsibilities;
|
||||
5. the successful-push completion point and separately reported delivery status;
|
||||
and
|
||||
6. failure handling and ambiguous-push recovery.
|
||||
|
||||
Use copyable commands where practical. Keep optional diagnostics and consumer
|
||||
verification clearly optional. The procedure must not turn preparation work,
|
||||
ordinary change review, or post-publication investigation into additional
|
||||
release gates.
|
||||
208
docs/release.md
208
docs/release.md
@@ -1,164 +1,84 @@
|
||||
# Source Releases
|
||||
|
||||
This procedure is for maintainers publishing Notarius source releases. A
|
||||
release is an immutable lightweight `vMAJOR.MINOR.PATCH` tag on `main` together
|
||||
with its checked-in `docs/releases/<tag>.md` note. Tag CI validates that source
|
||||
candidate after publication; it does not publish or repair a release.
|
||||
This is the executable runbook for [Release Policy](policy/release.md).
|
||||
Notarius publishes an immutable lightweight tag and its checked-in release note,
|
||||
not binaries or Gitea release objects. Linux is supported; macOS is best-effort
|
||||
for development, and Windows is unsupported. `main` on `origin` is authoritative.
|
||||
|
||||
Notarius publishes no binaries, archives, checksums, signatures, containers,
|
||||
package-manager entries, or Gitea release objects. Windows is not supported.
|
||||
Do not create retrospective notes for the pre-procedure `v0.1.0`, `v0.2.0`, or
|
||||
`v0.3.0` tags.
|
||||
## Prepare
|
||||
|
||||
## Select And Describe The Release
|
||||
|
||||
Choose an unused stable semantic version in the form `vMAJOR.MINOR.PATCH`.
|
||||
Prereleases are not supported. Before `v1.0.0`, a minor release may change a
|
||||
documented CLI, configuration, durable artifact, integration, or operating
|
||||
contract when its note explains the impact and required operator action. A
|
||||
patch release must not intentionally break those documented contracts within
|
||||
its minor line.
|
||||
|
||||
Create the version-matched note as part of the candidate. Every new note uses
|
||||
this structure, with concise, truthful content in each section:
|
||||
|
||||
```markdown
|
||||
# Notarius vMAJOR.MINOR.PATCH
|
||||
|
||||
This release ...
|
||||
|
||||
## Summary
|
||||
|
||||
## Compatibility
|
||||
|
||||
## Upgrade
|
||||
|
||||
## Changes
|
||||
```
|
||||
|
||||
The note is a historical summary. Link to current canonical documentation for
|
||||
exact behavior, and update that documentation in the candidate rather than
|
||||
using the note as a substitute.
|
||||
|
||||
## Prepare The Candidate
|
||||
|
||||
Set the selected release version and disable Go workspace use for every
|
||||
candidate command:
|
||||
Use Git, a POSIX shell, and Go 1.25.5 or newer. Configure ordinary Git credentials
|
||||
for `origin` and private-module access without putting secrets in the repository.
|
||||
Prepare dependencies before offline validation where necessary:
|
||||
|
||||
```sh
|
||||
RELEASE_VERSION=vMAJOR.MINOR.PATCH
|
||||
export RELEASE_VERSION GOWORK=off
|
||||
GOWORK=off GOPRIVATE=gitea.maximumdirect.net go mod download
|
||||
```
|
||||
|
||||
Run the shared source-candidate checks from the repository. They cover module
|
||||
hygiene, tests, race tests, vet, builds, formatting, whitespace, maintained
|
||||
configuration validation, and the Linux and Darwin command-build matrix:
|
||||
Choose an unused stable version under the policy's compatibility rules. Add
|
||||
`docs/releases/<version>.md`, starting with `# Notarius <version>`, with a concise
|
||||
summary and relevant compatibility/upgrade information. No fixed section list is
|
||||
required. Update current documentation with behavior changes; retire only
|
||||
completed roadmaps and preserve planned/deferred work. Commit the candidate on
|
||||
`main` after ordinary change review. There is no separate release sign-off.
|
||||
|
||||
## Publish
|
||||
|
||||
Run one command from the repository with the actual selected version:
|
||||
|
||||
```sh
|
||||
./scripts/check-release-source.sh "$RELEASE_VERSION"
|
||||
./scripts/release.sh vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
Before committing, manually follow every changed local Markdown link and
|
||||
review the candidate for unintended files, generated output, credentials, or
|
||||
other unrelated changes. Commit the release note and all affected current
|
||||
documentation, then run the shared checker against that exact candidate. Push
|
||||
the candidate commit to `main` only after it succeeds. Record the exact commit
|
||||
only after that push:
|
||||
The command checks version availability on `origin`, branch and worktree state,
|
||||
and the matching note. It runs the complete offline default test suite once and
|
||||
a native CLI build/version check with `GOWORK=off`. The
|
||||
[gate rationale](policy/release.md#notarius-gate-selection) explains the selected
|
||||
checks. It then rechecks candidate and publication state, pushes the exact
|
||||
candidate to `main` without force, creates a lightweight tag, and pushes only
|
||||
that tag. It neither rewrites history nor publishes other local tags.
|
||||
|
||||
A successful tag push completes the source release. The command does not wait
|
||||
for CI or verify consumer installation/module availability. Tag CI is advisory
|
||||
validation only; there are no asynchronous deliverables to await.
|
||||
|
||||
## Failure And Recovery
|
||||
|
||||
Before publication, correct failed checks, commit any changes, and rerun the
|
||||
command. If the candidate changes during validation, validation must run again
|
||||
on the new candidate. A failed branch push publishes no tag.
|
||||
|
||||
On a failed or ambiguous push, the command queries the affected remote ref. If
|
||||
a tag push reports failure but the intended lightweight tag is confirmed on the
|
||||
remote, publication succeeded. Otherwise it stops and reports failure or unknown
|
||||
status, leaving any local tag intact for inspection. Do not blindly rerun, force
|
||||
push, or delete a possibly published tag.
|
||||
|
||||
Inspect authoritative state before retrying:
|
||||
|
||||
```sh
|
||||
RELEASE_COMMIT=$(git rev-parse 'HEAD^{commit}')
|
||||
export RELEASE_COMMIT
|
||||
git ls-remote origin refs/heads/main refs/tags/vMAJOR.MINOR.PATCH
|
||||
git rev-parse vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
For private-module installation, configure standard `GOPRIVATE` matching this
|
||||
module and ordinary Git authentication for the hosting service before running
|
||||
the verification below. The exact authentication mechanism belongs to the
|
||||
maintainer environment; never record credentials or environment dumps in a
|
||||
release note, command history, or repository file.
|
||||
If the intended tag exists remotely, leave it immutable. If it is absent and
|
||||
you have confirmed that the local tag was never published or used by a release
|
||||
channel, remove only that unpublished local tag, then rerun the command. A
|
||||
failed remote query is not proof of absence. Network/authentication failures
|
||||
alone do not require a new version. A defect requiring changes to published
|
||||
source requires a new semantically appropriate version and matching note.
|
||||
|
||||
## Guard And Publish The Tag
|
||||
## Optional Diagnostics
|
||||
|
||||
Fetch current remote references, then run this guard without editing the
|
||||
candidate. It requires `main`, a clean worktree and index, disabled workspace
|
||||
use, a stable release version, the recorded and pushed commit, a matching note,
|
||||
and unused local and remote tags:
|
||||
`./scripts/check-release-source.sh <version>` runs validation without publication.
|
||||
It is useful for diagnosis, but is not a required preliminary step that must
|
||||
repeat the publication command's checks.
|
||||
|
||||
```sh
|
||||
git fetch origin main --tags
|
||||
Race tests, vet, module hygiene, formatting, example checks, and cross-builds may
|
||||
be run when relevant to a change; they are not additional release gates. The
|
||||
development guide and testing policy govern change-specific verification.
|
||||
|
||||
if ! printf '%s\n' "$RELEASE_VERSION" |
|
||||
grep -E -x 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' >/dev/null
|
||||
then
|
||||
printf '%s\n' "invalid release version: $RELEASE_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$GOWORK" = off
|
||||
test "$(git branch --show-current)" = main
|
||||
test -z "$(git status --porcelain)"
|
||||
test "$RELEASE_COMMIT" = "$(git rev-parse 'HEAD^{commit}')"
|
||||
test "$RELEASE_COMMIT" = "$(git rev-parse 'origin/main^{commit}')"
|
||||
test -s "docs/releases/$RELEASE_VERSION.md"
|
||||
grep -F -x "# Notarius $RELEASE_VERSION" "docs/releases/$RELEASE_VERSION.md"
|
||||
for heading in '## Summary' '## Compatibility' '## Upgrade' '## Changes'; do
|
||||
grep -F -x "$heading" "docs/releases/$RELEASE_VERSION.md"
|
||||
done
|
||||
if git rev-parse -q --verify "refs/tags/$RELEASE_VERSION" >/dev/null; then
|
||||
printf '%s\n' "local tag already exists: $RELEASE_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||
printf '%s\n' "remote tag already exists: $RELEASE_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
Create an explicitly lightweight tag against the guarded commit, verify its
|
||||
target, and push only that tag ref:
|
||||
|
||||
```sh
|
||||
git -c tag.gpgSign=false tag "$RELEASE_VERSION" "$RELEASE_COMMIT"
|
||||
test "$(git cat-file -t "$RELEASE_VERSION")" = commit
|
||||
test "$(git rev-parse "$RELEASE_VERSION^{commit}")" = "$RELEASE_COMMIT"
|
||||
git push origin "refs/tags/$RELEASE_VERSION:refs/tags/$RELEASE_VERSION"
|
||||
```
|
||||
|
||||
Never use `git push --tags`, move a published tag, or delete a published tag.
|
||||
|
||||
## Verify The Published Release
|
||||
|
||||
Confirm that the remote tag still points at the guarded commit and that the
|
||||
note is available from the tagged tree:
|
||||
|
||||
```sh
|
||||
REMOTE_TAG_COMMIT=$(git ls-remote origin "refs/tags/$RELEASE_VERSION" | awk '{print $1}')
|
||||
test "$REMOTE_TAG_COMMIT" = "$RELEASE_COMMIT"
|
||||
git show "$RELEASE_VERSION:docs/releases/$RELEASE_VERSION.md" >/dev/null
|
||||
```
|
||||
|
||||
Verify a fresh source installation and its diagnostic version. The temporary
|
||||
directory confines the installed command to this check:
|
||||
|
||||
```sh
|
||||
release_verification_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$release_verification_dir"' 0 HUP INT TERM
|
||||
mkdir -p "$release_verification_dir/bin"
|
||||
GOWORK=off GOBIN="$release_verification_dir/bin" go install \
|
||||
"gitea.maximumdirect.net/eric/notarius/cmd/notarius@$RELEASE_VERSION"
|
||||
test "$("$release_verification_dir/bin/notarius" --version)" = "notarius $RELEASE_VERSION"
|
||||
```
|
||||
|
||||
An exact fresh checkout and `GOWORK=off go build ./cmd/notarius` is an
|
||||
equivalent source verification when local installation policy requires it.
|
||||
`notarius --version` is diagnostic only; downstream compatibility remains
|
||||
defined by the published receipt and artifact contracts.
|
||||
|
||||
## Failure And Correction Policy
|
||||
|
||||
If candidate validation fails before publication, fix the candidate on `main`,
|
||||
rerun the shared checker, and repeat the guards. An unpublished local tag may
|
||||
be deleted after inspection.
|
||||
|
||||
If the remote tag or tag CI reveals a defect, leave the published tag intact.
|
||||
Fix the defect on `main`, choose a new patch version, write a new matching
|
||||
note, and repeat this procedure. Do not weaken tag immutability or add release
|
||||
assets as a workaround.
|
||||
A fresh source install or exact-tag checkout/build may be used to investigate
|
||||
consumer availability after release. Such checks are optional and do not delay
|
||||
or reverse release completion. `notarius --version` is diagnostic; subprocess
|
||||
compatibility is defined by the receipt and artifact contracts.
|
||||
|
||||
@@ -2,85 +2,23 @@
|
||||
|
||||
set -eu
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "check-release-source: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
fail "usage: $0 vMAJOR.MINOR.PATCH"
|
||||
fi
|
||||
|
||||
release_version=$1
|
||||
if ! printf '%s\n' "$release_version" | grep -E -x 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' >/dev/null; then
|
||||
fail "version must be a stable semantic version tag"
|
||||
fi
|
||||
fail() { printf '%s\n' "check-release-source: $*" >&2; exit 1; }
|
||||
|
||||
[ "$#" -eq 1 ] || fail "usage: $0 vMAJOR.MINOR.PATCH"
|
||||
version=$1
|
||||
printf '%s\n' "$version" | grep -E -x 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' >/dev/null || fail "invalid stable release version"
|
||||
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
|
||||
cd "$repo_root"
|
||||
cd "$script_dir/.."
|
||||
note="docs/releases/$version.md"
|
||||
[ -s "$note" ] || fail "missing release note: $note"
|
||||
[ "$(head -n 1 "$note")" = "# Notarius $version" ] || fail "release note title does not match version"
|
||||
|
||||
temporary_dir=$(mktemp -d "${TMPDIR:-/tmp}/notarius-release-source.XXXXXX") || fail "create temporary directory"
|
||||
trap 'rm -rf "$temporary_dir"' 0 HUP INT TERM
|
||||
export GOWORK=off
|
||||
go test -count=1 ./...
|
||||
|
||||
if ! grep -E '^module[[:space:]]+gitea\.maximumdirect\.net/eric/notarius([[:space:]]|$)' go.mod >/dev/null; then
|
||||
fail "go.mod does not declare the expected module path"
|
||||
fi
|
||||
|
||||
if git ls-files --error-unmatch go.work >/dev/null 2>&1 || git ls-files --error-unmatch go.work.sum >/dev/null 2>&1; then
|
||||
fail "tracked Go workspace files are not allowed"
|
||||
fi
|
||||
if [ -d vendor ]; then
|
||||
fail "vendor directory is not allowed"
|
||||
fi
|
||||
if grep -E '^[[:space:]]*replace([[:space:]]|$)' go.mod >/dev/null; then
|
||||
fail "go.mod replace directives are not allowed"
|
||||
fi
|
||||
|
||||
GOWORK=off go test -count=1 ./...
|
||||
GOWORK=off go test -race -count=1 ./...
|
||||
GOWORK=off go vet ./...
|
||||
GOWORK=off go build ./...
|
||||
GOWORK=off go mod tidy -diff
|
||||
|
||||
unformatted=$(git ls-files '*.go' | while IFS= read -r go_file; do
|
||||
fmt_output=$(gofmt -l "$go_file")
|
||||
if [ -n "$fmt_output" ]; then
|
||||
printf '%s\n' "$fmt_output"
|
||||
fi
|
||||
done)
|
||||
if [ -n "$unformatted" ]; then
|
||||
printf '%s\n' "$unformatted" >&2
|
||||
fail "tracked Go files are not gofmt formatted"
|
||||
fi
|
||||
|
||||
git diff --check
|
||||
git diff --cached --check
|
||||
|
||||
GOWORK=off go run ./cmd/notarius config validate --config examples/dnd-minimal.config.yml --pipeline dnd-session
|
||||
GOWORK=off OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session
|
||||
|
||||
host_os=$(GOWORK=off go env GOOS)
|
||||
host_arch=$(GOWORK=off go env GOARCH)
|
||||
version_symbol='gitea.maximumdirect.net/eric/notarius/internal/buildinfo.Override'
|
||||
|
||||
build_target() {
|
||||
target_os=$1
|
||||
target_arch=$2
|
||||
target_path="$temporary_dir/notarius-$target_os-$target_arch"
|
||||
CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" GOWORK=off go build -trimpath \
|
||||
-ldflags "-X $version_symbol=$release_version" \
|
||||
-o "$target_path" ./cmd/notarius
|
||||
if [ "$host_os" = "$target_os" ] && [ "$host_arch" = "$target_arch" ]; then
|
||||
"$target_path" --version > "$temporary_dir/version-output"
|
||||
printf 'notarius %s\n' "$release_version" > "$temporary_dir/expected-version-output"
|
||||
if ! cmp -s "$temporary_dir/expected-version-output" "$temporary_dir/version-output"; then
|
||||
fail "version output is incorrect for $target_os/$target_arch"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
build_target linux amd64
|
||||
build_target linux arm64
|
||||
build_target darwin amd64
|
||||
build_target darwin arm64
|
||||
# Exercise the actual command and its linker-supplied diagnostic version.
|
||||
verification_dir=$(mktemp -d "${TMPDIR:-/tmp}/notarius-release-check.XXXXXX")
|
||||
trap 'rm -rf "$verification_dir"' 0 HUP INT TERM
|
||||
go build -trimpath -ldflags "-X gitea.maximumdirect.net/eric/notarius/internal/buildinfo.Override=$version" \
|
||||
-o "$verification_dir/notarius" ./cmd/notarius
|
||||
[ "$("$verification_dir/notarius" --version)" = "notarius $version" ] || fail "incorrect diagnostic version"
|
||||
|
||||
56
scripts/release.sh
Executable file
56
scripts/release.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
fail() { printf '%s\n' "release: $*" >&2; exit 1; }
|
||||
|
||||
[ "$#" -eq 1 ] || fail "usage: $0 vMAJOR.MINOR.PATCH"
|
||||
version=$1
|
||||
printf '%s\n' "$version" | grep -E -x 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' >/dev/null || fail "invalid stable release version"
|
||||
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||
cd "$script_dir/.."
|
||||
export GOWORK=off
|
||||
candidate=$(git rev-parse 'HEAD^{commit}')
|
||||
|
||||
check_candidate() {
|
||||
[ "$(git branch --show-current)" = main ] || fail "release branch must be main"
|
||||
[ -z "$(git status --porcelain)" ] || fail "worktree and index must be clean"
|
||||
[ "$(git rev-parse 'HEAD^{commit}')" = "$candidate" ] || fail "candidate changed during validation"
|
||||
git cat-file -e "$candidate:docs/releases/$version.md" || fail "release note must be committed"
|
||||
}
|
||||
|
||||
check_publication() {
|
||||
# Query failure is not proof that the tag is unused.
|
||||
remote_tag=$(git ls-remote --tags origin "refs/tags/$version" "refs/tags/$version^{}") || fail "cannot determine remote tag availability"
|
||||
[ -z "$remote_tag" ] || fail "remote tag already exists: $version"
|
||||
if git show-ref --verify --quiet "refs/tags/$version"; then
|
||||
fail "local tag already exists: $version"
|
||||
fi
|
||||
git fetch --no-tags origin refs/heads/main || fail "cannot fetch authoritative main"
|
||||
remote_main=$(git rev-parse 'FETCH_HEAD^{commit}')
|
||||
git merge-base --is-ancestor "$remote_main" "$candidate" || fail "candidate does not contain authoritative main"
|
||||
}
|
||||
|
||||
check_candidate
|
||||
check_publication
|
||||
"$script_dir/check-release-source.sh" "$version"
|
||||
check_candidate
|
||||
check_publication
|
||||
|
||||
if ! git -c push.followTags=false push origin "$candidate:refs/heads/main"; then
|
||||
# Do not publish a tag after a failed branch push, even if the branch arrived.
|
||||
git ls-remote origin refs/heads/main >&2 || fail "branch publication status unknown; inspect origin before retrying"
|
||||
fail "branch push failed; inspect the reported remote state before retrying"
|
||||
fi
|
||||
|
||||
git -c tag.gpgSign=false tag "$version" "$candidate"
|
||||
[ "$(git cat-file -t "$version")" = commit ] || fail "tag is not lightweight"
|
||||
[ "$(git rev-parse "$version^{commit}")" = "$candidate" ] || fail "tag target changed"
|
||||
if ! git -c push.followTags=false push origin "refs/tags/$version:refs/tags/$version"; then
|
||||
# A connection can fail after the server accepted the tag. Preserve it locally
|
||||
# until authoritative state proves whether publication succeeded.
|
||||
remote_tag=$(git ls-remote --tags origin "refs/tags/$version" "refs/tags/$version^{}") || fail "tag publication status unknown; preserve local tag and inspect origin"
|
||||
expected=$(printf '%s\t%s' "$candidate" "refs/tags/$version")
|
||||
[ "$remote_tag" = "$expected" ] || fail "tag push failed; preserve local tag and inspect origin before retrying"
|
||||
fi
|
||||
printf 'Released %s at %s\n' "$version" "$candidate"
|
||||
243
scripts/release_test.go
Normal file
243
scripts/release_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Keep publication tests offline: real Git repositories exercise ref safety,
|
||||
// while a fake Go command represents the expensive candidate validation boundary.
|
||||
type releaseFixture struct {
|
||||
t *testing.T
|
||||
root string
|
||||
remote string
|
||||
bin string
|
||||
log string
|
||||
initial string
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path, content string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func command(t *testing.T, dir string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%v: %v\n%s", args, err, output)
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
func newReleaseFixture(t *testing.T) *releaseFixture {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
f := &releaseFixture{t: t, root: filepath.Join(dir, "work"), remote: filepath.Join(dir, "origin.git"), bin: filepath.Join(dir, "bin"), log: filepath.Join(dir, "validation.log")}
|
||||
command(t, dir, "git", "init", "--bare", f.remote)
|
||||
command(t, dir, "git", "init", "-b", "main", f.root)
|
||||
f.git("config", "user.name", "Release Test")
|
||||
f.git("config", "user.email", "release@example.invalid")
|
||||
f.git("config", "commit.gpgSign", "false")
|
||||
f.git("config", "tag.gpgSign", "false")
|
||||
for _, name := range []string{"release.sh", "check-release-source.sh"} {
|
||||
content, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, filepath.Join(f.root, "scripts", name), string(content), 0755)
|
||||
}
|
||||
writeFile(t, filepath.Join(f.root, ".gitignore"), "vendor/\n", 0644)
|
||||
f.git("add", ".")
|
||||
f.git("commit", "-m", "Initial source")
|
||||
f.git("remote", "add", "origin", f.remote)
|
||||
f.git("push", "origin", "main")
|
||||
f.initial = f.git("rev-parse", "HEAD")
|
||||
writeFile(t, filepath.Join(f.root, "docs/releases/v0.8.0.md"), "# Notarius v0.8.0\n\nA concise note without fixed sections.\n", 0644)
|
||||
f.git("add", ".")
|
||||
f.git("commit", "-m", "Prepare release")
|
||||
writeFile(t, filepath.Join(f.bin, "go"), `#!/bin/sh
|
||||
set -eu
|
||||
[ "$GOWORK" = off ]
|
||||
printf '%s\n' "$1" >> "$RELEASE_TEST_LOG"
|
||||
case "$1" in
|
||||
test)
|
||||
[ "${RELEASE_TEST_FAIL:-}" != test ] || exit 1
|
||||
if [ "${RELEASE_TEST_CHANGE:-}" = yes ]; then
|
||||
printf 'changed\n' >> docs/releases/v0.8.0.md
|
||||
fi
|
||||
;;
|
||||
build)
|
||||
while [ "$1" != -o ]; do shift; done
|
||||
shift
|
||||
printf '#!/bin/sh\nprintf "notarius v0.8.0\\n"\n' > "$1"
|
||||
chmod +x "$1"
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
`, 0755)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *releaseFixture) git(args ...string) string {
|
||||
f.t.Helper()
|
||||
return command(f.t, f.root, append([]string{"git"}, args...)...)
|
||||
}
|
||||
|
||||
func (f *releaseFixture) run(version string, env ...string) (string, error) {
|
||||
f.t.Helper()
|
||||
cmd := exec.Command("sh", "scripts/release.sh", version)
|
||||
cmd.Dir = f.root
|
||||
cmd.Env = append(os.Environ(), "PATH="+f.bin+string(os.PathListSeparator)+os.Getenv("PATH"), "RELEASE_TEST_LOG="+f.log)
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
func TestReleasePublishesExactCandidateOnce(t *testing.T) {
|
||||
f := newReleaseFixture(t)
|
||||
// Neither ignored local files nor unrelated local tags are release inputs.
|
||||
writeFile(t, filepath.Join(f.root, "vendor/local.txt"), "ignored", 0644)
|
||||
f.git("tag", "unrelated")
|
||||
f.git("tag", "-a", "unrelated-annotated", "-m", "Do not publish")
|
||||
f.git("config", "push.followTags", "true")
|
||||
candidate := f.git("rev-parse", "HEAD")
|
||||
output, err := f.run("v0.8.0")
|
||||
if err != nil {
|
||||
t.Fatalf("release: %v\n%s", err, output)
|
||||
}
|
||||
for _, ref := range []string{"refs/heads/main", "refs/tags/v0.8.0"} {
|
||||
if got := command(t, f.remote, "git", "rev-parse", ref); got != candidate {
|
||||
t.Fatalf("%s = %s, want %s", ref, got, candidate)
|
||||
}
|
||||
}
|
||||
if got := command(t, f.remote, "git", "cat-file", "-t", "v0.8.0"); got != "commit" {
|
||||
t.Fatalf("tag type = %s", got)
|
||||
}
|
||||
if got := f.git("ls-remote", "--tags", "origin", "refs/tags/unrelated*"); got != "" {
|
||||
t.Fatal("published unrelated tag")
|
||||
}
|
||||
log, err := os.ReadFile(f.log)
|
||||
if err != nil || string(log) != "test\nbuild\n" {
|
||||
t.Fatalf("validation was not performed once: %q, %v", log, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseGuardsPreventPublication(t *testing.T) {
|
||||
for _, name := range []string{"invalid_version", "missing_note", "wrong_title", "dirty", "staged", "wrong_branch", "local_tag", "remote_tag", "remote_ahead", "remote_unavailable", "validation_failure", "candidate_changed"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
f := newReleaseFixture(t)
|
||||
version := "v0.8.0"
|
||||
var env []string
|
||||
switch name {
|
||||
case "invalid_version":
|
||||
version = "v00.8.0"
|
||||
case "missing_note":
|
||||
version = "v0.8.1"
|
||||
case "wrong_title":
|
||||
writeFile(t, filepath.Join(f.root, "docs/releases/v0.8.0.md"), "# Wrong version\n", 0644)
|
||||
f.git("add", ".")
|
||||
f.git("commit", "-m", "Wrong title")
|
||||
case "dirty", "staged":
|
||||
writeFile(t, filepath.Join(f.root, "dirty.txt"), "dirty", 0644)
|
||||
if name == "staged" {
|
||||
f.git("add", ".")
|
||||
}
|
||||
case "wrong_branch":
|
||||
f.git("checkout", "-b", "other")
|
||||
case "local_tag":
|
||||
f.git("tag", version)
|
||||
case "remote_tag":
|
||||
f.git("push", "origin", f.initial+":refs/tags/"+version)
|
||||
case "remote_ahead":
|
||||
// Publish an independent descendant of the old remote tip.
|
||||
f.git("checkout", "-b", "other", f.initial)
|
||||
f.git("commit", "--allow-empty", "-m", "Other work")
|
||||
f.git("push", "origin", "HEAD:main")
|
||||
f.initial = f.git("rev-parse", "HEAD")
|
||||
f.git("checkout", "main")
|
||||
case "remote_unavailable":
|
||||
f.git("remote", "set-url", "origin", filepath.Join(t.TempDir(), "missing.git"))
|
||||
case "validation_failure":
|
||||
env = []string{"RELEASE_TEST_FAIL=test"}
|
||||
case "candidate_changed":
|
||||
env = []string{"RELEASE_TEST_CHANGE=yes"}
|
||||
}
|
||||
if output, err := f.run(version, env...); err == nil {
|
||||
t.Fatalf("guard allowed publication: %s", output)
|
||||
}
|
||||
if got := command(t, f.remote, "git", "rev-parse", "main"); got != f.initial {
|
||||
t.Fatal("guard advanced remote branch")
|
||||
}
|
||||
if name != "remote_tag" {
|
||||
if got := command(t, f.remote, "git", "tag", "--list"); got != "" {
|
||||
t.Fatalf("guard published tag: %s", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleasePushFailureRecovery(t *testing.T) {
|
||||
for _, mode := range []string{"branch_rejected", "tag_rejected", "tag_accepted_disconnect", "tag_unknown"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
f := newReleaseFixture(t)
|
||||
if mode == "branch_rejected" || mode == "tag_rejected" {
|
||||
pattern := "refs/heads/main"
|
||||
if mode == "tag_rejected" {
|
||||
pattern = "refs/tags/*"
|
||||
}
|
||||
writeFile(t, filepath.Join(f.remote, "hooks/update"), "#!/bin/sh\ncase \"$1\" in "+pattern+") exit 1;; esac\n", 0755)
|
||||
} else {
|
||||
realGit, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Simulate a lost acknowledgement, optionally followed by an outage.
|
||||
writeFile(t, filepath.Join(f.bin, "git"), `#!/bin/sh
|
||||
set -eu
|
||||
if [ "$1" = -c ] && [ "$3" = push ] && [ "$5" = refs/tags/v0.8.0:refs/tags/v0.8.0 ]; then
|
||||
"$RELEASE_TEST_GIT" "$@"
|
||||
touch "$RELEASE_TEST_MARKER"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$1" = ls-remote ] && [ -e "$RELEASE_TEST_MARKER" ] && [ "$RELEASE_TEST_MODE" = tag_unknown ]; then
|
||||
exit 1
|
||||
fi
|
||||
exec "$RELEASE_TEST_GIT" "$@"
|
||||
`, 0755)
|
||||
output, err := f.run("v0.8.0", "RELEASE_TEST_GIT="+realGit, "RELEASE_TEST_MARKER="+filepath.Join(f.bin, "marker"), "RELEASE_TEST_MODE="+mode)
|
||||
if (err == nil) != (mode == "tag_accepted_disconnect") {
|
||||
t.Fatalf("unexpected outcome: %v\n%s", err, output)
|
||||
}
|
||||
if got := command(t, f.remote, "git", "rev-parse", "v0.8.0"); got != f.git("rev-parse", "HEAD") {
|
||||
t.Fatal("wrong remote tag")
|
||||
}
|
||||
return
|
||||
}
|
||||
if output, err := f.run("v0.8.0"); err == nil {
|
||||
t.Fatalf("rejected push reported success: %s", output)
|
||||
}
|
||||
if got := command(t, f.remote, "git", "tag", "--list"); got != "" {
|
||||
t.Fatal("rejected push published tag")
|
||||
}
|
||||
if mode == "tag_rejected" {
|
||||
if got := f.git("rev-parse", "v0.8.0"); got != f.git("rev-parse", "HEAD") {
|
||||
t.Fatal("unpublished local tag not preserved")
|
||||
}
|
||||
} else if got := f.git("tag", "--list"); got != "" {
|
||||
t.Fatal("created tag after failed branch push")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user