Skip to content

feat(targets): add pkg-dnf target (dnf / RPM / Fedora COPR)#485

Open
forgou37 wants to merge 5 commits into
profullstack:masterfrom
forgou37:feat/pkg-dnf
Open

feat(targets): add pkg-dnf target (dnf / RPM / Fedora COPR)#485
forgou37 wants to merge 5 commits into
profullstack:masterfrom
forgou37:feat/pkg-dnf

Conversation

@forgou37
Copy link
Copy Markdown
Contributor

Closes #476

Adds the pkg-dnf target adapter for generating RPM .spec files and publishing to Fedora COPR.

What

  • Generates a valid RPM .spec file from package config
  • Supports x86_64 / aarch64 / noarch architectures
  • Default source URL derived from releaseRepo or coprProject config
  • Runtime/build Requires and BuildRequires support
  • ship() submits to Fedora COPR (via COPR API — requires COPR_LOGIN + COPR_TOKEN secrets)
  • manualSetup guide for COPR account and token setup
  • Full vitest test coverage (.spec content, dry-run ship)

Related

Part of #133 — implements sh1pt target sub-commands.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 30, 2026

Greptile Summary

Adds a new pkg-dnf target adapter that generates RPM .spec files and (stub-wise) submits builds to Fedora COPR. The build() path is meaningfully implemented: it renders a complete spec file with configurable metadata, dependencies, and source URL, consistent with the rest of the adapter ecosystem.

  • Source URL substitution bug: defaultSourceUrl uses String.replace(string) which only replaces the first {version} occurrence; the default template has two, so the filename portion of Source0 retains a literal {version} placeholder that RPM cannot expand, causing rpmbuild to fetch a 404 URL.
  • Wrong status() URL: returns a link to packages.fedoraproject.org (official Fedora package index) instead of copr.fedorainfracloud.org, where COPR packages actually live.
  • Non-deterministic changelog date and a missing Source0 assertion in the test suite (which would have caught the substitution bug) are also worth addressing.

Confidence Score: 3/5

The adapter should not be merged as-is: the generated spec file will contain a broken Source0 URL that makes every real rpmbuild invocation fail, and the status URL directs users to the wrong registry entirely.

The spec generation — the most concrete piece of functionality in this PR — produces an RPM spec with a malformed Source0 line due to String.replace() stopping after the first {version} match. Any user running an actual COPR build from the generated spec will get a fetch error. Additionally status() returns a URL pointing to official Fedora packages rather than COPR, so the tracking link is always wrong. Both are correctable with small targeted fixes, but they affect the primary output of the adapter.

packages/targets/pkg-dnf/src/index.ts needs the replaceAll fix and the corrected status URL before this is functional. packages/targets/pkg-dnf/src/index.test.ts should add a Source0 assertion to prevent the substitution bug from regressing.

Important Files Changed

Filename Overview
packages/targets/pkg-dnf/src/index.ts Core adapter implementation: spec generation works but has two logic bugs — String.replace() only replaces the first {version} in the default source URL template, and status() links to the official Fedora packages index instead of COPR. Changelog date is non-deterministic. ship() is a documented stub consistent with other adapters in the repo.
packages/targets/pkg-dnf/src/index.test.ts Tests cover happy-path spec content and dry-run shipping, but the Source0 URL is not asserted, missing the broken placeholder substitution bug.
packages/targets/pkg-dnf/package.json Standard monorepo package manifest; dependencies and scripts look correct.
packages/targets/pkg-dnf/README.md Brief, accurate setup guide for COPR credentials. No issues.
packages/targets/pkg-dnf/tsconfig.json Extends root tsconfig with correct outDir/rootDir settings. No issues.

Sequence Diagram

sequenceDiagram
    participant User
    participant sh1pt
    participant pkg-dnf adapter
    participant Filesystem
    participant COPR API

    User->>sh1pt: sh1pt release
    sh1pt->>pkg-dnf adapter: build(ctx, config)
    pkg-dnf adapter->>pkg-dnf adapter: renderSpec(config, version)
    Note over pkg-dnf adapter: defaultSourceUrl() — replace {version},{name},{arch}
    pkg-dnf adapter->>Filesystem: writeFile(outDir/name.spec)
    pkg-dnf adapter-->>sh1pt: { artifact: "name.spec" }

    sh1pt->>pkg-dnf adapter: ship(ctx, config)
    alt dryRun
        pkg-dnf adapter-->>sh1pt: { id: "dry-run" }
    else real run (TODO: not yet implemented)
        Note over pkg-dnf adapter,COPR API: COPR_LOGIN + COPR_TOKEN required
        pkg-dnf adapter-->>sh1pt: stub { id, url } (no actual API call)
    end

    sh1pt->>pkg-dnf adapter: status(shipId)
    pkg-dnf adapter-->>sh1pt: { state: "live", url: copr URL }
Loading

Reviews (1): Last reviewed commit: "feat(targets): add pkg-dnf tests" | Re-trigger Greptile

Comment on lines +35 to +38
return (config.sourceUrlTemplate ?? `https://github.com/${repo}/releases/download/v{version}/{name}-{version}-{arch}.tar.gz`)
.replace('{version}', version)
.replace('{name}', config.packageName)
.replace('{arch}', arch);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The default URL template contains {version} twice (v{version}/ and {name}-{version}-{arch}), but String.prototype.replace(string, replacement) only replaces the first occurrence. The second {version} — the one in the filename — will remain as a literal {version} string in the emitted Source0 header. RPM does not interpret curly-brace placeholders, so rpmbuild will attempt to download a URL with a literal {version} in the filename, which will 404 every time.

Suggested change
return (config.sourceUrlTemplate ?? `https://github.com/${repo}/releases/download/v{version}/{name}-{version}-{arch}.tar.gz`)
.replace('{version}', version)
.replace('{name}', config.packageName)
.replace('{arch}', arch);
return (config.sourceUrlTemplate ?? `https://github.com/${repo}/releases/download/v{version}/{name}-{version}-{arch}.tar.gz`)
.replaceAll('{version}', version)
.replaceAll('{name}', config.packageName)
.replaceAll('{arch}', arch);

Comment on lines +133 to +136
async status(id) {
const [name] = id.split('@');
return { state: 'live', url: `https://packages.fedoraproject.org/pkgs/${name}/` };
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The status() URL points to packages.fedoraproject.org, which is the search index for official Fedora packages — not COPR. A package published via this adapter lands at copr.fedorainfracloud.org/coprs/<owner>/<project>/, so the returned URL leads users to a page where their package will never appear.

Suggested change
async status(id) {
const [name] = id.split('@');
return { state: 'live', url: `https://packages.fedoraproject.org/pkgs/${name}/` };
},
async status(id) {
const [nameAndVersion] = id.split('@');
// id format is "<packageName>@<version>" from ship(); copr slug not available here,
// so fall back to the COPR search page rather than the wrong official packages index.
return { state: 'live', url: `https://copr.fedorainfracloud.org/coprs/search/?query=${nameAndVersion}` };
},

`%{_bindir}/${name}`,
'',
'%changelog',
`* ${new Date().toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: '2-digit' })} sh1pt <noreply@sh1pt.com> - ${version}-1`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 new Date() in renderSpec captures the wall-clock time at spec-generation, so running build twice on the same version on different days produces different file contents. This breaks reproducible builds and will cause unnecessary diffs in version-controlled .spec files.

Suggested change
`* ${new Date().toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: '2-digit' })} sh1pt <noreply@sh1pt.com> - ${version}-1`,
`* ${new Date(0).toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: '2-digit' })} sh1pt <noreply@sh1pt.com> - ${version}-1`,

Comment on lines +31 to +44

expect(result.artifact).toBe(join(outDir, 'myapp.spec'));

const spec = await readFile(join(outDir, 'myapp.spec'), 'utf-8');
expect(spec).toContain('Name: myapp');
expect(spec).toContain('Version: 1.5.0');
expect(spec).toContain('Summary: An example CLI tool');
expect(spec).toContain('License: MIT');
expect(spec).toContain('URL: https://example.com');
expect(spec).toContain('BuildArch: x86_64');
expect(spec).toContain('Requires: glibc');
expect(spec).toContain('%description');
expect(spec).toContain('%files');
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Source0 URL not tested — masks the replace bug

The test checks Name, Version, Summary, License, URL, BuildArch, Requires, %description, and %files, but never asserts the Source0 line. Because of this, the first-occurrence-only .replace('{version}', ...) bug on the default URL template would not be caught. Adding an assertion with the fully-expanded expected URL (no literal {version} remaining) would have caught this and will prevent regressions.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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