Ensure frontend reports git-aware version strings (#321)

* Ensure frontend reports git-aware version strings

* Keep footer fixed across viewport widths
This commit is contained in:
l5y
2025-10-13 16:26:57 +02:00
committed by GitHub
parent 2642ff7a95
commit ffad84f18a
4 changed files with 173 additions and 44 deletions
+26 -4
View File
@@ -19,9 +19,8 @@ module PotatoMesh
#
# @return [String] semantic version compatible identifier.
def determine_app_version
repo_root = File.expand_path("../../..", __dir__)
git_dir = File.join(repo_root, ".git")
return PotatoMesh::Config.version_fallback unless File.directory?(git_dir)
repo_root = locate_git_repo_root(File.expand_path("../../..", __dir__))
return PotatoMesh::Config.version_fallback unless repo_root
stdout, status = Open3.capture2("git", "-C", repo_root, "describe", "--tags", "--long", "--abbrev=7")
return PotatoMesh::Config.version_fallback unless status.success?
@@ -42,6 +41,29 @@ module PotatoMesh
PotatoMesh::Config.version_fallback
end
# Discover the root directory of the git repository containing the
# application by traversing parent directories until a ``.git`` entry is
# located. This supports both traditional repositories where ``.git`` is a
# directory and worktree checkouts where it is a plain file.
#
# @param start_dir [String] absolute path where the search should begin.
# @return [String, nil] absolute path to the repository root when found,
# otherwise ``nil``.
def locate_git_repo_root(start_dir)
current = File.expand_path(start_dir)
loop do
git_entry = File.join(current, ".git")
return current if File.exist?(git_entry)
parent = File.dirname(current)
break if parent == current
current = parent
end
nil
end
# Load the persisted instance private key or generate a new one when absent.
#
# @return [Array<OpenSSL::PKey::RSA, Boolean>] tuple of key and generation flag.
@@ -111,7 +133,7 @@ module PotatoMesh
end
end
private :migrate_legacy_keyfile_for_identity!
private :migrate_legacy_keyfile_for_identity!, :locate_git_repo_root
# Return the directory used to store well-known documents.
#
+57 -7
View File
@@ -126,7 +126,7 @@ tbody tr:nth-child(even) td {
body {
font-family: system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif;
margin: var(--pad);
padding-bottom: 32px;
padding-bottom: 96px;
--map-tiles-filter: var(--map-tile-filter-light);
}
@@ -740,16 +740,66 @@ input[type="radio"] {
font-size: 12px;
}
footer {
.app-footer {
position: fixed;
bottom: 0;
left: var(--pad);
width: calc(100% - 2 * var(--pad));
inset-block-end: 0;
inset-inline: 0;
width: 100%;
background: #fafafa;
border-top: 1px solid #ddd;
text-align: center;
font-size: 12px;
padding: 4px 0;
z-index: 4100;
}
.app-footer .footer-content {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
margin: 0 auto;
width: 100%;
max-width: 960px;
padding: 6px var(--pad);
text-align: center;
box-sizing: border-box;
}
.app-footer .footer-separator {
margin: 0 4px;
}
.app-footer .footer-links {
display: inline-flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
justify-content: center;
}
.app-footer .footer-brand {
font-weight: 600;
}
.app-footer a {
color: inherit;
}
@media (max-width: 600px) {
.app-footer .footer-content {
padding: 10px 12px;
justify-content: center;
gap: 4px 8px;
}
.app-footer .footer-links {
flex-direction: column;
gap: 4px;
}
.app-footer .footer-separator {
display: none;
}
}
.info-overlay {
+68 -20
View File
@@ -408,65 +408,106 @@ RSpec.describe "Potato Mesh Sinatra app" do
end
end
describe "#determine_app_version" do
let(:repo_root) { File.expand_path("..", __dir__) }
let(:git_dir) { File.join(repo_root, ".git") }
describe ".locate_git_repo_root" do
it "returns nil when a git directory cannot be found" do
nested_dir = Dir.mktmpdir("potato-mesh-no-git-")
begin
deep_dir = File.join(nested_dir, "a", "b", "c")
FileUtils.mkdir_p(deep_dir)
before do
allow(File).to receive(:directory?).and_call_original
result = application_class.send(:locate_git_repo_root, deep_dir)
expect(result).to be_nil
ensure
FileUtils.remove_entry(nested_dir)
end
end
it "returns the fallback when the git directory is missing" do
allow(File).to receive(:directory?).with(git_dir).and_return(false)
it "locates a git directory" do
nested_dir = Dir.mktmpdir("potato-mesh-with-git-")
begin
repo_root = File.join(nested_dir, "repo")
FileUtils.mkdir_p(File.join(repo_root, ".git"))
deep_dir = File.join(repo_root, "lib", "potato")
FileUtils.mkdir_p(deep_dir)
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
result = application_class.send(:locate_git_repo_root, deep_dir)
expect(result).to eq(repo_root)
ensure
FileUtils.remove_entry(nested_dir)
end
end
it "recognises git worktree files" do
nested_dir = Dir.mktmpdir("potato-mesh-worktree-")
begin
repo_root = File.join(nested_dir, "worktree")
FileUtils.mkdir_p(repo_root)
File.write(File.join(repo_root, ".git"), "gitdir: /tmp/worktree")
deep_dir = File.join(repo_root, "app", "lib")
FileUtils.mkdir_p(deep_dir)
result = application_class.send(:locate_git_repo_root, deep_dir)
expect(result).to eq(repo_root)
ensure
FileUtils.remove_entry(nested_dir)
end
end
end
describe "#determine_app_version" do
let(:repo_root) { File.expand_path("..", __dir__) }
it "returns the fallback when the git directory is missing" do
allow(application_class).to receive(:locate_git_repo_root).and_return(nil)
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
end
it "returns the fallback when git describe fails" do
allow(File).to receive(:directory?).with(git_dir).and_return(true)
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
status = instance_double(Process::Status, success?: false)
allow(Open3).to receive(:capture2).and_return(["ignored", status])
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
end
it "returns the fallback when git describe output is empty" do
allow(File).to receive(:directory?).with(git_dir).and_return(true)
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture2).and_return(["\n", status])
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
end
it "returns the original describe output when the format is unexpected" do
allow(File).to receive(:directory?).with(git_dir).and_return(true)
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture2).and_return(["weird-output", status])
expect(determine_app_version).to eq("weird-output")
expect(application_class.determine_app_version).to eq("weird-output")
end
it "normalises the version when no commits are ahead of the tag" do
allow(File).to receive(:directory?).with(git_dir).and_return(true)
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture2).and_return(["v1.2.3-0-gabcdef1", status])
expect(determine_app_version).to eq("v1.2.3")
expect(application_class.determine_app_version).to eq("v1.2.3")
end
it "includes commit metadata when ahead of the tag" do
allow(File).to receive(:directory?).with(git_dir).and_return(true)
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture2).and_return(["v1.2.3-5-gabcdef1", status])
expect(determine_app_version).to eq("v1.2.3+5-abcdef1")
expect(application_class.determine_app_version).to eq("v1.2.3+5-abcdef1")
end
it "returns the fallback when git describe raises an error" do
allow(File).to receive(:directory?).with(git_dir).and_return(true)
allow(application_class).to receive(:locate_git_repo_root).and_return(repo_root)
allow(Open3).to receive(:capture2).and_raise(StandardError, "boom")
expect(determine_app_version).to eq(PotatoMesh::Config.version_fallback)
expect(application_class.determine_app_version).to eq(PotatoMesh::Config.version_fallback)
end
end
@@ -764,6 +805,13 @@ RSpec.describe "Potato Mesh Sinatra app" do
expect(last_response.body).to include("#{APP_VERSION}")
end
it "renders the responsive footer container" do
get "/"
expect(last_response.body).to include('<footer class="app-footer">')
expect(last_response.body).to include('class="footer-content"')
end
it "includes SEO metadata from configuration" do
allow(PotatoMesh::Config).to receive(:site_name).and_return("Spec Mesh Title")
allow(PotatoMesh::Config).to receive(:channel).and_return("#SpecChannel")
+22 -13
View File
@@ -196,20 +196,29 @@
</div>
</template>
<footer>
PotatoMesh
<% if version && !version.empty? %>
<span class="mono"><%= version %></span> —
<% end %>
GitHub: <a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
<% if contact_link && !contact_link.empty? %>
— <%= site_name %> chat:
<% if contact_link_url %>
<a href="<%= contact_link_url %>" target="_blank"><%= contact_link %></a>
<% else %>
<%= contact_link %>
<footer class="app-footer">
<div class="footer-content">
<span class="footer-brand">PotatoMesh</span>
<% if version && !version.empty? %>
<span class="mono"><%= version %></span>
<% end %>
<% end %>
<span class="footer-separator" aria-hidden="true">—</span>
<span class="footer-links">
GitHub:
<a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
<% if contact_link && !contact_link.empty? %>
<span class="footer-separator" aria-hidden="true">—</span>
<span class="footer-contact">
<%= site_name %> chat:
<% if contact_link_url %>
<a href="<%= contact_link_url %>" target="_blank"><%= contact_link %></a>
<% else %>
<%= contact_link %>
<% end %>
</span>
<% end %>
</span>
</div>
</footer>