Skip to content

Commit c0192b6

Browse files
authored
Merge pull request #41 from NDDev-OpenNetwork/fix/release-semantics-070
fix(release): preserve worktree and prerelease identity
2 parents e320622 + 6bf1799 commit c0192b6

20 files changed

Lines changed: 246 additions & 38 deletions

.gds/bundle.lock.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ bundle:
55
version: "0.4.0-dev"
66
release_sequence: 0
77
channel: "development"
8-
source_tree_digest: "sha256:48f2b1985e221205b7f0528948ff7dd7483b332f11afc33ca0f118af6eaf3554"
9-
digest: "sha256:1463b23f250b5204041dd4f5de12c3a390f8a9a84b38e46c0a01d0e3eee9af8a"
8+
source_tree_digest: "sha256:1aa9237afbbce4e74a7e2580ad22601a8ce2c2c5046cb797e6cfc82aa8320e2d"
9+
digest: "sha256:eda29a8be938e2ea6dd85b28b9e0bb30cec155ea31386cfd2b043ce8b959cc66"
1010

1111
projection:
12-
input_digest: "sha256:453a5f58141f6206c3fe136e56c193275d7eade3ccb1e30fd6aa555f5a93cef8"
13-
output_digest: "sha256:4dbaf721de0e61c8429766bf4449a31635eccd1ab179d1ff996c15fa33310b6a"
12+
input_digest: "sha256:491245684963cbacff4fe8c5c3b828b5f367b08bda217a264d7617b592cfeff3"
13+
output_digest: "sha256:be645e1600d99a870a126567eb5ec2690c3faec7664d8196eebdea567b458d1c"
1414
files:
1515
- path: ".gds/compiled-policy.json"
1616
digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94"
1717
- path: ".github/workflows/gds-ci.yml"
18-
digest: "sha256:932c2ba6b4db499a2e7549ab9ee46085a7fa386f34b5b3a1837c93487eef119c"
18+
digest: "sha256:3994ebccb515b10763ecbe69b4a31fc8e34a09adf7e5543a10c8716b761e4e1c"

.github/workflows/gds-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# GENERATED FILE - DO NOT EDIT DIRECTLY
22
# generator: gds
33
# bundle: 0.4.0-dev
4-
# source-tree-digest: sha256:48f2b1985e221205b7f0528948ff7dd7483b332f11afc33ca0f118af6eaf3554
5-
# input-digest: sha256:453a5f58141f6206c3fe136e56c193275d7eade3ccb1e30fd6aa555f5a93cef8
4+
# source-tree-digest: sha256:1aa9237afbbce4e74a7e2580ad22601a8ce2c2c5046cb797e6cfc82aa8320e2d
5+
# input-digest: sha256:491245684963cbacff4fe8c5c3b828b5f367b08bda217a264d7617b592cfeff3
66
# output-digest: sha256:c268237f2cffde69b7868265ba6ac331048c98036fb9030ee7b7b3a208a9ad49
77
# edit-source:
88
# - .gds/repository.yaml

.github/workflows/release-bundle.yml

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,67 @@ jobs:
330330
RELEASE_CHANNEL: ${{ inputs.channel }}
331331
run: |
332332
set -euo pipefail
333-
latest_flags=()
334-
[ "$RELEASE_CHANNEL" = "stable" ] && latest_flags=(--latest)
333+
release_flags=()
334+
case "$RELEASE_CHANNEL" in
335+
stable) release_flags=(--latest) ;;
336+
canary) release_flags=(--prerelease) ;;
337+
esac
335338
gh release create "$RELEASE_TAG" \
336339
--repo "$GITHUB_REPOSITORY" \
337340
--title "GDS $RELEASE_TAG" \
338341
--verify-tag \
339-
"${latest_flags[@]}" \
342+
"${release_flags[@]}" \
340343
--notes "Immutable GDS ${RELEASE_CHANNEL} bundle for ${RELEASE_TAG}. Multi-platform binaries + SPDX SBOM + SHA256SUMS, with keyless SLSA build provenance and SBOM attestations. Online: gh attestation verify <asset> -R ${GITHUB_REPOSITORY}. Offline: verify <asset> against provenance.sigstore.json and sbom.sigstore.json using trusted-root.jsonl, all attached here." \
341344
"$RELEASE_DIRECTORY"/* \
342345
"$EVIDENCE_DIRECTORY"/*
346+
347+
record-failure:
348+
name: Record failed release evidence
349+
needs: [build, attest, publish]
350+
if: ${{ always() && startsWith(github.ref, 'refs/tags/') && (needs.build.result == 'failure' || needs.attest.result == 'failure' || needs.publish.result == 'failure') }}
351+
runs-on: ubuntu-latest
352+
permissions:
353+
contents: write
354+
env:
355+
GH_TOKEN: ${{ github.token }}
356+
RELEASE_TAG: ${{ github.ref_name }}
357+
RELEASE_VERSION: ${{ inputs.version }}
358+
RELEASE_SEQUENCE: ${{ inputs.release_sequence }}
359+
RELEASE_CHANNEL: ${{ inputs.channel }}
360+
BUILD_RESULT: ${{ needs.build.result }}
361+
ATTEST_RESULT: ${{ needs.attest.result }}
362+
PUBLISH_RESULT: ${{ needs.publish.result }}
363+
steps:
364+
- name: Publish machine-readable failure envelope
365+
shell: bash
366+
run: |
367+
set -euo pipefail
368+
envelope="$RUNNER_TEMP/release-failure-envelope.json"
369+
failed_jobs="$RUNNER_TEMP/failed-jobs.json"
370+
jq -cn \
371+
--arg build "$BUILD_RESULT" \
372+
--arg attest "$ATTEST_RESULT" \
373+
--arg publish "$PUBLISH_RESULT" \
374+
'[{name:"build",result:$build},{name:"attest",result:$attest},{name:"publish",result:$publish}] | map(select(.result == "failure") | .name)' \
375+
> "$failed_jobs"
376+
jq -n \
377+
--arg version "$RELEASE_VERSION" \
378+
--argjson sequence "$RELEASE_SEQUENCE" \
379+
--arg channel "$RELEASE_CHANNEL" \
380+
--arg commit "$GITHUB_SHA" \
381+
--arg ref "$GITHUB_REF" \
382+
--argjson run_id "$GITHUB_RUN_ID" \
383+
--slurpfile failed "$failed_jobs" \
384+
'{schema_version:1,status:"failed",bundle_version:$version,release_sequence:$sequence,channel:$channel,source_commit:$commit,source_ref:$ref,workflow_run_id:$run_id,failed_jobs:$failed[0],superseded_by:null}' \
385+
> "$envelope"
386+
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
387+
gh release upload "$RELEASE_TAG" "$envelope" --repo "$GITHUB_REPOSITORY"
388+
else
389+
gh release create "$RELEASE_TAG" \
390+
--repo "$GITHUB_REPOSITORY" \
391+
--title "GDS $RELEASE_TAG (failed)" \
392+
--verify-tag \
393+
--prerelease \
394+
--notes "Release production failed. See release-failure-envelope.json for machine-readable evidence; retain this tag and release until a later release records it as superseded." \
395+
"$envelope"
396+
fi

core/bundle/bundle_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ func TestVerifyAttestationAndAntiRollbackPolicy(t *testing.T) {
151151
t.Fatalf("exact rollback authorization was not accepted: result=%#v findings=%#v", result, findings)
152152
}
153153

154+
forward := candidate.Envelope
155+
forward.ReleaseSequence = 3
156+
forward.BundleVersion = "0.9.0"
157+
state = AcceptanceState{
158+
HighestSequence: 2,
159+
AcceptedDigests: map[int]string{2: digest(bytes.Repeat([]byte{'b'}, 64))},
160+
AcceptedVersions: map[int]string{2: "1.0.0"},
161+
}
162+
result, findings = Verify(forward, testTrust(), evidence, state, nil, now)
163+
assertFinding(t, findings, "GDS_BUNDLE_VERSION_REGRESSION")
164+
if result.Status != "quarantined" {
165+
t.Fatalf("semantic version regression was not quarantined: %#v", result)
166+
}
167+
154168
evidence.Verified = false
155169
result, findings = Verify(candidate.Envelope, testTrust(), evidence, AcceptanceState{}, nil, now)
156170
assertFinding(t, findings, "GDS_BUNDLE_ATTESTATION_INVALID")

core/bundle/types.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ type AttestationEvidence struct {
150150
}
151151

152152
type AcceptanceState struct {
153-
HighestSequence int `json:"highest_sequence"`
154-
AcceptedDigests map[int]string `json:"accepted_digests"`
153+
HighestSequence int `json:"highest_sequence"`
154+
AcceptedDigests map[int]string `json:"accepted_digests"`
155+
AcceptedVersions map[int]string `json:"accepted_versions,omitempty"`
155156
}
156157

157158
type RollbackAuthorization struct {

core/bundle/verify.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/NDDev-OpenNetwork/github-device-sync/core/domain"
9+
"github.com/NDDev-OpenNetwork/github-device-sync/core/semver"
910
)
1011

1112
func Verify(
@@ -101,6 +102,16 @@ func Verify(
101102
"A release sequence is already bound to a different artifact digest.",
102103
))
103104
}
105+
if envelope.ReleaseSequence > state.HighestSequence && state.HighestSequence > 0 {
106+
if previous := state.AcceptedVersions[state.HighestSequence]; previous != "" {
107+
if compared, valid := semver.Compare(envelope.BundleVersion, previous); !valid || compared < 0 {
108+
findings = append(findings, verificationFinding(
109+
"GDS_BUNDLE_VERSION_REGRESSION",
110+
"A higher release sequence cannot lower Semantic Version precedence.",
111+
))
112+
}
113+
}
114+
}
104115
status := "accepted"
105116
if len(findings) != 0 {
106117
status = "quarantined"

core/cli/root_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,8 @@ func commitCLITestRepository(t *testing.T, root string, message string) {
10031003
{"init", "--quiet"},
10041004
{"config", "user.name", "GDS CLI test"},
10051005
{"config", "user.email", "cli-test@example.invalid"},
1006+
{"config", "gc.auto", "0"},
1007+
{"config", "maintenance.auto", "false"},
10061008
{"add", "--all"},
10071009
{"commit", "--quiet", "-m", message},
10081010
} {

core/providers/git/status.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,6 @@ func (runner *Runner) InspectStatus(ctx context.Context, directory string) (Stat
161161
}
162162
status.Repository = info
163163
status.Worktrees = parseWorktrees(worktreeResult.Stdout)
164-
for index := range status.Worktrees {
165-
if status.Worktrees[index].Path == info.CommonGitDir &&
166-
info.WorktreeRoot != info.CommonGitDir {
167-
status.Worktrees[index].Path = info.WorktreeRoot
168-
}
169-
}
170164
status.Submodules = parseSubmodules(submoduleResult.Stdout)
171165
status.RemoteFreshness = "unknown"
172166
status.Classification = classify(status)

core/providers/git/status_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,25 @@ func TestInspectStatusDetachedAndMultipleWorktrees(t *testing.T) {
278278
if detached.Head.Mode != "detached" || detached.Classification != "detached" {
279279
t.Fatalf("detached status = %#v", detached)
280280
}
281+
paths := map[string]bool{}
282+
for _, worktree := range detached.Worktrees {
283+
canonical, err := filepath.EvalSymlinks(worktree.Path)
284+
if err != nil {
285+
t.Fatal(err)
286+
}
287+
paths[canonical] = true
288+
}
289+
directory, err = filepath.EvalSymlinks(directory)
290+
if err != nil {
291+
t.Fatal(err)
292+
}
293+
linked, err = filepath.EvalSymlinks(linked)
294+
if err != nil {
295+
t.Fatal(err)
296+
}
297+
if len(paths) != 2 || !paths[directory] || !paths[linked] {
298+
t.Fatalf("linked worktree paths = %#v, want %q and %q", paths, directory, linked)
299+
}
281300
}
282301

283302
func TestInspectStatusCachedAheadBehindAndDiverged(t *testing.T) {

core/releasebuilder/workflow_contract_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ func TestHostedReleaseWorkflowUsesOutputOutsideSourceRoot(t *testing.T) {
9797
`stable/frozen requires signed active-five harness evidence`,
9898
`--harness-evidence-directory $EVIDENCE_INPUT_ROOT/records`,
9999
`RELEASE_SEQUENCE: ${{ inputs.release_sequence }}`,
100+
`canary) release_flags=(--prerelease) ;;`,
101+
`name: Record failed release evidence`,
102+
`release-failure-envelope.json`,
103+
`superseded_by:null`,
100104
} {
101105
if !strings.Contains(content, required) {
102106
t.Fatalf("hosted workflow is missing output contract %q", required)

0 commit comments

Comments
 (0)