Skip to content

fix: always write a nuspec into generated NuGet packages - #664

Open
NickJosevski wants to merge 2 commits into
mainfrom
nj/fix-nuspec-always
Open

fix: always write a nuspec into generated NuGet packages#664
NickJosevski wants to merge 2 commits into
mainfrom
nj/fix-nuspec-always

Conversation

@NickJosevski

Copy link
Copy Markdown
Contributor

Actions #632 (comment), raised by @YuKitsune while reviewing #632. Pre-existing on main, so it is here rather than there.

Problem

package nuget create only generated the .nuspec when the caller supplied a description, title, release notes or author. Ask for nothing but an ID and a version and the command reports success, but the .nupkg has no manifest in it — a valid zip, and not a valid NuGet package:

$ octopus package nuget create --id Acme.Web --version 1.2.3 --base-path src --no-prompt
Successfully created package Acme.Web.1.2.3.nupkg

$ unzip -l Acme.Web.1.2.3.nupkg
        6  app.txt          <-- that is the whole package

Readers locate the manifest by looking for a single .nuspec in the archive root, so there is nothing for them to find. #632 makes this sharper: buildOpcParts writes _rels/.rels with a relationship pointing at /{id}.nuspec unconditionally, so the package also carries a reference to a part that was never created.

Change

Generate a manifest for every package:

$ unzip -l Acme.Web.1.2.3.nupkg
      313  Acme.Web.nuspec
        6  app.txt

$ unzip -p Acme.Web.1.2.3.nupkg Acme.Web.nuspec
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>Acme.Web</id>
    <version>1.2.3</version>
    <description>A deployment package created from files on disk.</description>
    <authors>nickj</authors>
  </metadata>
</package>

The schema requires description and authors alongside id and version, so both now default instead of being filled in only when the caller had already opted into metadata:

default note
description A deployment package created from files on disk. already what --description help promises
authors the current OS user what the Author flag's own comment says it should be, and what the old Octopus CLI did

The author default is the debatable part — it puts a machine username into package metadata for anyone who does not pass --author. The alternatives were omitting the element (schema-invalid) or a placeholder. Happy to change it. If the user cannot be determined, which happens on minimal container images, the package ID stands in so the command still succeeds.

Not clobbering a hand-written nuspec

Generating unconditionally introduced a way to destroy someone's file: GenerateNuSpec writes to {basePath}/{id}.nuspec and the caller deletes that path afterwards. Anyone keeping a hand-written manifest in their base path would have had it overwritten and then removed. A supplied nuspec is now detected, packed as-is, and never touched.

Second commit: XML escaping

The manifest is assembled by string concatenation, so an ampersand or angle bracket in a description or release notes produced a document no XML parser would accept — fixed A & B is enough. Survivable while the nuspec was opt-in; now it is on the path everyone takes.

Separate commit (5d04abf) so it can be dropped if you would rather see it on its own.

Verification

15 unit tests in pkg/cmd/package/nuget/create/create_test.go, asserting against parsed XML rather than substrings. They cover the manifest being generated with no metadata supplied, both defaults, a supplied nuspec being detected, the user-lookup fallback, and escaped values round-tripping.

Checked end to end against a built binary: the before/after unzip output above is real, a hand-written nuspec survives byte-identical (diff -q clean) and is the one that gets packed, and the generated file is cleaned up from the base path afterwards.

Interaction with #632

Both touch create.go but in different places — this one around nuspec generation, #632 around the BuildPackageWithContents call. Whichever lands second may need a trivial rebase. Once both are in, _rels/.rels points at a part that always exists.

🤖 Generated with Claude Code

NickJosevski and others added 2 commits August 6, 2026 11:54
The manifest was only generated when the caller supplied a description,
title, release notes or author. Ask for nothing but an ID and a version
and the command still reported success, but produced a .nupkg with no
.nuspec in it: a valid zip, and not a valid NuGet package. Readers
locate the manifest by looking for a single .nuspec in the archive root,
so there was nothing for them to find.

Generate one for every package. The nuspec schema requires description
and authors alongside id and version, so both now default rather than
being filled in only when the caller had already opted into metadata.
The description default is the one the --description flag has always
documented, and the author default is the current user, which is what
the flag's own comment says it should be and what the old Octopus CLI
did. Pass --author to override it.

A base path that already contains {id}.nuspec is left alone and packed
as-is. That file is the user's own: generating over the top would
discard their metadata, and the cleanup step would then delete it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest is assembled by string concatenation, so an ampersand or
angle bracket in a description, title or release notes produced a
document no XML parser would accept. Release notes in particular are
free text, and "fixed A & B" is enough to do it.

This was survivable while the nuspec was only written for callers who
had opted into metadata. Now that one is generated for every package it
is on the path everyone takes, so escape each value on the way in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant