plan_my_stuff 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06744d7c7bc63206522ce6d7c9176eeb8e93fabb4de903d7df32b17cfae6948d
4
- data.tar.gz: 2338f2f85989b2ee3b29a48c114a82d36c8105c13e4caf19b2601ff02cbf063c
3
+ metadata.gz: 0d33c652f54d9d078276d12baac63134e8bba4064185f61626c66fd924ea958a
4
+ data.tar.gz: 54f5c69130890fa5ae807d642cd8d89d2f0d74c4d12b638ed892d8f9b2df22d8
5
5
  SHA512:
6
- metadata.gz: dd143db048f77a46fe7875b1592b9ff1dc26e6ddac13af60c177ae977f799606d172a23ca861222a1a133d53fb91d16be19dd40d8f7027b680316276d99088f0
7
- data.tar.gz: 607957cf66e887c31bb6ab3f9a93726da0cf853cb3d43e8d34857b0dd789981788de4f0c8c6e076ad5236141732867ed9641971dcc683df5e4683e6b726542f3
6
+ metadata.gz: 36d2a9a1624279d818d6c1a16fcfd51cd8e047e4f02d51044673fecb0ef3433b4a953290eb51deb868b9a8959fe365abf4dcb0645883466941047c6915c070d3
7
+ data.tar.gz: fbadf2e2e8337a7014e148e898291de7ef4453729332f2c8d2609942487ebec7359a97a687e44340edac1a0d7c8e36fdeddcd3cc90c8fe30a108f76f7ba74a49
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Breaking
6
+
7
+ - `PMS::Attachment` no longer accepts/stores a single `url:` attribute. Construct with explicit
8
+ `filename:`, `owner:`, `repo:`, `sha:`, `path:` instead; `#url` is now a derived method that returns the
9
+ GitHub blob viewer URL (`https://github.com/{owner}/{repo}/blob/{sha}/{path}`) rather than the
10
+ `raw.githubusercontent.com` URL. `#to_h` emits the structured fields and no longer includes a `url` key.
11
+ Persisted comment metadata in the legacy `{filename, url}` shape is migrated on read and rewritten in the
12
+ new shape on next save (closes #70).
13
+
14
+ ### Added
15
+
16
+ - Comment bodies now render a visible `<details><summary>attachments (N)</summary>` block listing each
17
+ attachment as a markdown link to its blob URL, between the `pms-metadata` block and the user body. The
18
+ parser strips this block on read so round-trips stay clean.
19
+
3
20
  ## 0.20.0
4
21
 
5
22
  ### Breaking
@@ -6,33 +6,40 @@ require 'tmpdir'
6
6
  module PlanMyStuff
7
7
  # Value object representing a single attachment record on a +Comment+.
8
8
  # Persisted in +CommentMetadata#attachments+; the gem owns the upload
9
- # (see +PlanMyStuff::AttachmentUploader+) and stores a SHA-pinned
10
- # +raw.githubusercontent.com+ permalink so the file remains reachable
11
- # across later branch rewrites.
9
+ # (see +PlanMyStuff::AttachmentUploader+) and stores the structured
10
+ # location (+owner+/+repo+/+sha+/+path+) of the uploaded file so the
11
+ # blob remains addressable across later branch rewrites.
12
12
  #
13
13
  # Mirrors +PlanMyStuff::Link+ / +PlanMyStuff::Approval+:
14
14
  # +ActiveModel::Attributes+-backed, with +Serializers::JSON+ for
15
15
  # round-trip through the metadata blob.
16
16
  #
17
17
  class Attachment
18
- RAW_URL_REGEX =
19
- %r{\Ahttps://raw\.githubusercontent\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/(?<sha>[^/\s]+)/(?<path>.+)\z}
20
-
21
18
  include ActiveModel::Model
22
19
  include ActiveModel::Attributes
23
20
  include ActiveModel::Serializers::JSON
24
21
 
25
22
  # @return [String] display filename (user-provided original name)
26
23
  attribute :filename, :string
27
- # @return [String] SHA-pinned raw.githubusercontent.com permalink to the uploaded file
28
- attribute :url, :string
24
+ # @return [String] owner of the attachment repo (e.g. +"BrandsInsurance"+)
25
+ attribute :owner, :string
26
+ # @return [String] attachment repo name
27
+ attribute :repo, :string
28
+ # @return [String] commit SHA pinning the file
29
+ attribute :sha, :string
30
+ # @return [String] path within the attachment repo (no leading slash)
31
+ attribute :path, :string
32
+
33
+ validates :filename, :owner, :repo, :sha, :path, presence: true
29
34
 
30
- validates :filename, presence: true
31
- validates :url, presence: true, format: { with: RAW_URL_REGEX }
35
+ # @return [String] blob-viewer URL for the pinned file
36
+ def url
37
+ "https://github.com/#{owner}/#{repo}/blob/#{sha}/#{path}"
38
+ end
32
39
 
33
40
  # @return [Hash]
34
41
  def to_h
35
- { filename: filename, url: url }
42
+ { filename: filename, owner: owner, repo: repo, sha: sha, path: path }
36
43
  end
37
44
 
38
45
  # @param other [Object]
@@ -56,22 +63,21 @@ module PlanMyStuff
56
63
  # Defaults the destination to +Dir.tmpdir+ joined with +File.basename(filename)+ (directory components
57
64
  # stripped to prevent path traversal).
58
65
  #
59
- # @param path [String, nil] destination path; defaults to +File.join(Dir.tmpdir, File.basename(filename))+
66
+ # @param dest [String, nil] destination path; defaults to +File.join(Dir.tmpdir, File.basename(filename))+
60
67
  #
61
68
  # @return [String] the path the bytes were written to
62
69
  #
63
- def download_to(path = nil)
64
- path ||= File.join(Dir.tmpdir, File.basename(filename.to_s))
65
- match = RAW_URL_REGEX.match(url)
70
+ def download_to(dest = nil)
71
+ dest ||= File.join(Dir.tmpdir, File.basename(filename.to_s))
66
72
  body = PlanMyStuff.client.rest(
67
73
  :contents,
68
- "#{match[:owner]}/#{match[:repo]}",
69
- path: match[:path],
70
- ref: match[:sha],
74
+ "#{owner}/#{repo}",
75
+ path: path,
76
+ ref: sha,
71
77
  accept: 'application/vnd.github.raw',
72
78
  )
73
- File.binwrite(path, body)
74
- path
79
+ File.binwrite(dest, body)
80
+ dest
75
81
  end
76
82
  end
77
83
  end
@@ -6,8 +6,8 @@ require 'securerandom'
6
6
 
7
7
  module PlanMyStuff
8
8
  # Uploads attachment files to a shared attachment repo (+config.attachment_repo+ under +config.organization+) using
9
- # GitHub's Git Data API, returning SHA-pinned +raw.githubusercontent.com+ permalinks wrapped in
10
- # +PlanMyStuff::Attachment+ instances.
9
+ # GitHub's Git Data API, returning +PlanMyStuff::Attachment+ instances populated with the structured
10
+ # +owner+/+repo+/+sha+/+path+ location of each uploaded file.
11
11
  #
12
12
  # One commit per batch (atomic): all files in a single +upload_all!+ call land in the same tree/commit so partial
13
13
  # failures cannot leave +config.main_branch+ in an inconsistent state.
@@ -202,8 +202,13 @@ module PlanMyStuff
202
202
  # @return [PlanMyStuff::Attachment]
203
203
  #
204
204
  def build_attachment(commit_sha:, slot:)
205
- url = "https://raw.githubusercontent.com/#{attachment_repo_full_name}/#{commit_sha}/#{slot[:path]}"
206
- PlanMyStuff::Attachment.new(filename: slot[:filename], url: url)
205
+ PlanMyStuff::Attachment.new(
206
+ filename: slot[:filename],
207
+ owner: PlanMyStuff.configuration.organization,
208
+ repo: PlanMyStuff.configuration.attachment_repo,
209
+ sha: commit_sha,
210
+ path: slot[:path],
211
+ )
207
212
  end
208
213
 
209
214
  # @return [String] +"<organization>/<attachment_repo>"+
@@ -2,6 +2,17 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  class CommentMetadata < PlanMyStuff::BaseMetadata
5
+ LEGACY_URL_REGEXES = [
6
+ %r{\Ahttps://raw\.githubusercontent\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/(?<sha>[^/\s]+)/(?<path>.+)\z},
7
+ %r{\Ahttps://github\.com/(?<owner>[^/\s]+)/(?<repo>[^/\s]+)/blob/(?<sha>[^/\s]+)/(?<path>.+)\z},
8
+ ].freeze
9
+
10
+ LEGACY_ATTACHMENT_DEPRECATION_MESSAGE =
11
+ 'PlanMyStuff: legacy attachment metadata shape {filename, url} detected. It will continue to parse ' \
12
+ 'until 1.0.0, at which point legacy detection will be removed. New writes use the structured ' \
13
+ '{filename, owner, repo, sha, path} shape introduced in #70; existing entries migrate on their ' \
14
+ 'next write.'
15
+
5
16
  # @return [Boolean] true if this comment holds the issue's body content
6
17
  attr_accessor :issue_body
7
18
  # @return [Array<PlanMyStuff::Attachment>] consuming-app attachment records associated with this comment
@@ -82,9 +93,14 @@ module PlanMyStuff
82
93
  private
83
94
 
84
95
  # Builds a +PlanMyStuff::Attachment+ from each parsed entry.
85
- # Each entry is a +{filename:, url:}+ hash (symbol keys when
86
- # built locally, string keys when read back from GitHub
87
- # metadata) or an existing +PlanMyStuff::Attachment+.
96
+ # Accepts:
97
+ # - an existing +PlanMyStuff::Attachment+
98
+ # - the new shape +{filename:, owner:, repo:, sha:, path:}+
99
+ # - the legacy shape +{filename:, url:}+ where +url+ is a raw
100
+ # +raw.githubusercontent.com+ permalink or a +github.com/.../blob/...+
101
+ # blob-viewer URL; parsed once into structured fields so the
102
+ # next write drops the +url+ key (migrate-on-write).
103
+ #
88
104
  # Malformed hash entries are silently dropped so a single bad
89
105
  # historical entry doesn't crash +Comment.find+. An already-
90
106
  # constructed +Attachment+ that fails validation raises so the
@@ -102,7 +118,7 @@ module PlanMyStuff
102
118
  if entry.is_a?(PlanMyStuff::Attachment)
103
119
  entry
104
120
  elsif entry.respond_to?(:transform_keys)
105
- PlanMyStuff::Attachment.new(entry.transform_keys(&:to_sym))
121
+ PlanMyStuff::Attachment.new(structured_attrs(entry.transform_keys(&:to_sym)))
106
122
  end
107
123
  next if attachment.nil?
108
124
 
@@ -114,5 +130,33 @@ module PlanMyStuff
114
130
  next
115
131
  end
116
132
  end
133
+
134
+ # Returns +attrs+ untouched when it already carries structured
135
+ # fields, otherwise parses a legacy +url+ into
136
+ # +owner+/+repo+/+sha+/+path+. Unparseable URLs yield a hash
137
+ # with no structured fields, leaving +Attachment+ validation to
138
+ # drop the entry.
139
+ #
140
+ # @param attrs [Hash{Symbol=>Object}]
141
+ #
142
+ # @return [Hash{Symbol=>Object}]
143
+ #
144
+ def structured_attrs(attrs)
145
+ return attrs if attrs.key?(:owner)
146
+
147
+ PlanMyStuff.deprecator.warn(LEGACY_ATTACHMENT_DEPRECATION_MESSAGE)
148
+
149
+ url = attrs[:url].to_s
150
+ match = LEGACY_URL_REGEXES.lazy.filter_map { |re| re.match(url) }.first
151
+
152
+ return attrs.except(:url) if match.nil?
153
+
154
+ attrs.except(:url).merge(
155
+ owner: match[:owner],
156
+ repo: match[:repo],
157
+ sha: match[:sha],
158
+ path: match[:path],
159
+ )
160
+ end
117
161
  end
118
162
  end
@@ -18,6 +18,15 @@ module PlanMyStuff
18
18
  </details>\n*
19
19
  }mx
20
20
 
21
+ # Visible attachments block emitted after the metadata block when the
22
+ # metadata carries non-empty +attachments+ (issue #70). Parse strips it
23
+ # so round-trips stay clean.
24
+ ATTACHMENTS_PATTERN = %r{
25
+ \A<details><summary>attachments\ \(\d+\)</summary>\n\n
26
+ .*?\n\n
27
+ </details>\n*
28
+ }mx
29
+
21
30
  # Legacy format kept for parsing only - existing issues serialized before 0.17.0
22
31
  # used a hidden HTML comment. They migrate to the new format on the next write.
23
32
  LEGACY_METADATA_PATTERN = /\A<!-- pms-metadata:(.*?) -->\n*/m
@@ -40,7 +49,7 @@ module PlanMyStuff
40
49
 
41
50
  match = raw_body.match(pattern)
42
51
  metadata = JSON.parse(match[1], symbolize_names: true)
43
- body = raw_body.sub(pattern, '')
52
+ body = raw_body.sub(pattern, '').sub(ATTACHMENTS_PATTERN, '')
44
53
 
45
54
  { metadata: metadata, body: body }
46
55
  rescue JSON::ParserError
@@ -61,14 +70,37 @@ module PlanMyStuff
61
70
  raise(ArgumentError, "metadata must be a Hash or PlanMyStuff::CustomFields, got #{metadata.class}")
62
71
  end
63
72
 
64
- json =
65
- if metadata.is_a?(PlanMyStuff::CustomFields)
66
- JSON.pretty_generate(metadata.to_h)
67
- else
68
- JSON.pretty_generate(metadata)
69
- end
73
+ hash = metadata.is_a?(PlanMyStuff::CustomFields) ? metadata.to_h : metadata
74
+ json = JSON.pretty_generate(hash)
75
+
76
+ "<details><summary>pms-metadata</summary>\n\n```json\n#{json}\n```\n\n</details>\n\n" \
77
+ "#{attachments_block(hash)}#{body}"
78
+ end
70
79
 
71
- "<details><summary>pms-metadata</summary>\n\n```json\n#{json}\n```\n\n</details>\n\n#{body}"
80
+ # Renders the visible attachments block when +metadata+ carries
81
+ # non-empty +:attachments+, otherwise returns an empty string.
82
+ #
83
+ # @param metadata [Hash]
84
+ #
85
+ # @return [String]
86
+ #
87
+ def attachments_block(metadata)
88
+ attachments = metadata[:attachments]
89
+ return '' if attachments.blank?
90
+
91
+ lines = attachments.map { |a| attachment_line(a) }.join("\n")
92
+ "<details><summary>attachments (#{attachments.size})</summary>\n\n#{lines}\n\n</details>\n\n"
93
+ end
94
+
95
+ # @param attachment [Hash{Symbol=>String}]
96
+ #
97
+ # @return [String]
98
+ #
99
+ def attachment_line(attachment)
100
+ url = "https://github.com/#{attachment[:owner]}/#{attachment[:repo]}" \
101
+ "/blob/#{attachment[:sha]}/#{attachment[:path]}"
102
+ safe_filename = attachment[:filename].to_s.gsub(']', '\]')
103
+ "- [#{safe_filename}](#{url})"
72
104
  end
73
105
 
74
106
  # @param raw_body [String]
@@ -3,7 +3,7 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 20
6
+ MINOR = 21
7
7
  TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plan_my_stuff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance