Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit96da5ae

Browse files
ci: addupdate-appcast script (#171)
Third PR for#47.Adds a script to update an existing `appcast.xml`.This will be called in CI to update the appcast before uploading it back to our feed URL (`releases.coder.com/...`). It's currently not used anywhere.Invoked like:```swift run update-appcast -i appcast.xml -s CoderDesktop.pkg.sig -v 0.5.1 -o appcast.xml -d ${{ github.event.release.body }}```To update an appcast that looks like:<details><summary>appcast.xml</summary>```xml<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0"> <channel> <title>Coder Desktop</title> <item> <title>v0.5.1</title> <description><![CDATA[<h2>What's Changed</h2><ul><li>fix: don't create http client if signed out by@ethanndickson in <a href="https://github.com/coder/coder-deskt%E2%80%A6r-desktop-macos/pull/170">https://github.com/coder/coder-deskt…r-desktop-macos/pull/170</a></li></ul><p><strong>Full Changelog</strong>: <a href="https://github.com/coder/coder-desktop-macos/compare/v0.5.0...v0.5.1">https://github.com/coder/coder-desktop-macos/compare/v0.5.0...v0.5.1</a></p>]]></description> <pubDate>Thu, 29 May 2025 06:08:56 +0000</pubDate> <sparkle:channel>stable</sparkle:channel> <sparkle:version>0.5.1</sparkle:version> <sparkle:fullReleaseNotesLink>https://github.com/coder/coder-desktop-macos/releases</sparkle:fullReleaseNotesLink> <sparkle:minimumSystemVersion>14.0.0</sparkle:minimumSystemVersion> <enclosure url="https://github.com/coder/coder-desktop-macos/releases/download/v0.5.1/Coder-Desktop.pkg" type="application/octet-stream" sparkle:installationType="package" sparkle:edSignature="NkyCj7Lzpw95P0N95SQHiBCjDLZYVukbRR3aOjGZAuL5Dc+I//DfTCRFCxoQNhA38uu/CCAR8v9E4SgMkDdmAA==" length="39630183"></enclosure> </item> <item> <title>Preview</title> <pubDate>Thu, 29 May 2025 06:08:08 +0000</pubDate> <sparkle:channel>preview</sparkle:channel> <sparkle:version>0.5.0.3</sparkle:version> <sparkle:fullReleaseNotesLink>https://github.com/coder/coder-desktop-macos/releases</sparkle:fullReleaseNotesLink> <sparkle:minimumSystemVersion>14.0.0</sparkle:minimumSystemVersion> <enclosure url="https://github.com/coder/coder-desktop-macos/releases/download/preview/Coder-Desktop.pkg" type="application/octet-stream" sparkle:installationType="package" sparkle:edSignature="L0cFeyoy+D/Zgm3eXok87SKmgIUka8m2b+g7UWPReF4UhFUb4RlDsZ5PxXKd5MrtsaODGUz2iRMWraO7aQg+DA==" length="39630898"></enclosure> </item> </channel></rss>```</details>Producing a notification like:<img width="620" alt="image" src="https://github.com/user-attachments/assets/acae89d6-5d39-4464-bf60-7beac66af9c7" />
1 parent65f4619 commit96da5ae

File tree

5 files changed

+249
-2
lines changed

5 files changed

+249
-2
lines changed

‎.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ xcuserdata
291291
**/xcshareddata/WorkspaceSettings.xcsettings
292292

293293
### VSCode & Sweetpad ###
294-
.vscode/**
294+
**/.vscode/**
295295
buildServer.json
296296

297297
# End of https://www.toptal.com/developers/gitignore/api/xcode,jetbrains,macos,direnv,swift,swiftpm,objective-c

‎.swiftlint.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# TODO: Remove this once the grpc-swift-protobuf generator adds a lint disable comment
22
excluded:
33
-"**/*.pb.swift"
4-
-"**/*.grpc.swift"
4+
-"**/*.grpc.swift"
5+
-"**/.build/"

‎scripts/update-appcast/.swiftlint.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
disabled_rules:
2+
-todo
3+
-trailing_comma

‎scripts/update-appcast/Package.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
letpackage=Package(
7+
name:"update-appcast",
8+
platforms:[
9+
.macOS(.v15),
10+
],
11+
dependencies:[
12+
.package(url:"https://github.com/apple/swift-argument-parser", from:"1.3.0"),
13+
.package(url:"https://github.com/loopwerk/Parsley", from:"0.5.0"),
14+
],
15+
targets:[
16+
.executableTarget(
17+
name:"update-appcast", dependencies:[
18+
.product(name:"ArgumentParser",package:"swift-argument-parser"),
19+
.product(name:"Parsley",package:"Parsley"),
20+
]
21+
),
22+
]
23+
)
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import ArgumentParser
2+
import Foundation
3+
import RegexBuilder
4+
#if canImport(FoundationXML)
5+
import FoundationXML
6+
#endif
7+
import Parsley
8+
9+
/// UpdateAppcast
10+
/// -------------
11+
/// Replaces an existing `<item>` for the **stable** or **preview** channel
12+
/// in a Sparkle RSS feed with one containing the new version, signature, and
13+
/// length attributes. The feed will always contain one item for each channel.
14+
/// Whether the passed version is a stable or preview version is determined by the
15+
/// number of components in the version string:
16+
/// - Stable: `X.Y.Z`
17+
/// - Preview: `X.Y.Z.N`
18+
/// `N` is the build number - the number of commits since the last stable release.
19+
@main
20+
structUpdateAppcast:AsyncParsableCommand{
21+
staticletconfiguration=CommandConfiguration(
22+
abstract:"Updates a Sparkle appcast with a new release entry."
23+
)
24+
25+
@Option(name:.shortAndLong, help:"Path to the appcast file to be updated.")
26+
varinput:String
27+
28+
@Option(
29+
name:.shortAndLong,
30+
help:"""
31+
Path to the signature file generated for the release binary.
32+
Signature files are generated by `Sparkle/bin/sign_update
33+
"""
34+
)
35+
varsignature:String
36+
37+
@Option(name:.shortAndLong, help:"The project version (X.Y.Z for stable builds, X.Y.Z.N for preview builds).")
38+
varversion:String
39+
40+
@Option(name:.shortAndLong, help:"A description of the release written in GFM.")
41+
vardescription:String?
42+
43+
@Option(name:.shortAndLong, help:"Path where the updated appcast should be written.")
44+
varoutput:String
45+
46+
mutatingfunc validate()throws{
47+
guardFileManager.default.fileExists(atPath: signature)else{
48+
throwValidationError("No file exists at path\(signature).")
49+
}
50+
guardFileManager.default.fileExists(atPath: input)else{
51+
throwValidationError("No file exists at path\(input).")
52+
}
53+
}
54+
55+
// swiftlint:disable:next function_body_length
56+
mutatingfunc run()asyncthrows{
57+
letchannel:UpdateChannel=isStable(version: version)?.stable:.preview
58+
letsigLine=tryString(contentsOfFile: signature, encoding:.utf8)
59+
.trimmingCharacters(in:.whitespacesAndNewlines)
60+
61+
guardlet match= sigLine.firstMatch(of: signatureRegex)else{
62+
throwRuntimeError("Unable to parse signature file:\(sigLine)")
63+
}
64+
65+
letedSignature= match.output.1
66+
guardlet length= match.output.2else{
67+
throwRuntimeError("Unable to parse length from signature file.")
68+
}
69+
70+
letxmlData=tryData(contentsOf:URL(fileURLWithPath: input))
71+
letdoc=tryXMLDocument(data: xmlData, options:.nodePrettyPrint)
72+
73+
guardlet channelElem=try doc.nodes(forXPath:"/rss/channel").firstas?XMLElementelse{
74+
throwRuntimeError("<channel> element not found in appcast.")
75+
}
76+
77+
guardlet insertionIndex=(channelElem.children??[])
78+
.enumerated()
79+
.first(where:{ _, nodein
80+
guardlet item= nodeas?XMLElement,
81+
item.name=="item",
82+
item.elements(forName:"sparkle:channel")
83+
.first?.stringValue== channel.rawValue
84+
else{returnfalse}
85+
returntrue
86+
})?.offset
87+
else{
88+
throwRuntimeError("No existing item found for channel\(channel.rawValue).")
89+
}
90+
// Delete the existing item
91+
channelElem.removeChild(at: insertionIndex)
92+
93+
letitem=XMLElement(name:"item")
94+
switch channel{
95+
case.stable:
96+
item.addChild(XMLElement(name:"title", stringValue:"v\(version)"))
97+
case.preview:
98+
item.addChild(XMLElement(name:"title", stringValue:"Preview"))
99+
}
100+
101+
iflet description{
102+
letdescription= description.replacingOccurrences(of:#"\r\n"#, with:"\n")
103+
letdescriptionDoc:Document
104+
do{
105+
descriptionDoc=tryParsley.parse(description)
106+
}catch{
107+
throwRuntimeError("Failed to parse GFM description:\(error)")
108+
}
109+
// <description><![CDATA[ …HTML… ]]></description>
110+
letdescriptionElement=XMLElement(name:"description")
111+
letcdata=XMLNode(kind:.text, options:.nodeIsCDATA)
112+
lethtml= descriptionDoc.body
113+
114+
cdata.stringValue= html
115+
descriptionElement.addChild(cdata)
116+
item.addChild(descriptionElement)
117+
}
118+
119+
item.addChild(XMLElement(name:"pubDate", stringValue:rfc822Date()))
120+
item.addChild(XMLElement(name:"sparkle:channel", stringValue: channel.rawValue))
121+
item.addChild(XMLElement(name:"sparkle:version", stringValue: version))
122+
item.addChild(XMLElement(
123+
name:"sparkle:fullReleaseNotesLink",
124+
stringValue:"https://github.com/coder/coder-desktop-macos/releases"
125+
))
126+
item.addChild(XMLElement(
127+
name:"sparkle:minimumSystemVersion",
128+
stringValue:"14.0.0"
129+
))
130+
131+
letenclosure=XMLElement(name:"enclosure")
132+
func addEnclosureAttr(_ name:String, _ value:String){
133+
// Force-casting is the intended API usage.
134+
// swiftlint:disable:next force_cast
135+
enclosure.addAttribute(XMLNode.attribute(withName: name, stringValue: value)as!XMLNode)
136+
}
137+
addEnclosureAttr("url",downloadURL(for: version, channel: channel))
138+
addEnclosureAttr("type","application/octet-stream")
139+
addEnclosureAttr("sparkle:installationType","package")
140+
addEnclosureAttr("sparkle:edSignature", edSignature)
141+
addEnclosureAttr("length",String(length))
142+
item.addChild(enclosure)
143+
144+
channelElem.insertChild(item, at: insertionIndex)
145+
146+
letoutputStr= doc.xmlString(options:[.nodePrettyPrint])+"\n"
147+
try outputStr.write(to:URL(fileURLWithPath: output), atomically:true, encoding:.utf8)
148+
}
149+
150+
privatefunc isStable(version:String)->Bool{
151+
// A version is a release version if it has three components (X.Y.Z)
152+
guardlet match= version.firstMatch(of: versionRegex)else{returnfalse}
153+
return match.output.4==nil
154+
}
155+
156+
privatefunc downloadURL(for version:String, channel:UpdateChannel)->String{
157+
switch channel{
158+
case.stable:"https://github.com/coder/coder-desktop-macos/releases/download/v\(version)/Coder-Desktop.pkg"
159+
case.preview:"https://github.com/coder/coder-desktop-macos/releases/download/preview/Coder-Desktop.pkg"
160+
}
161+
}
162+
163+
privatefunc rfc822Date(date:Date=Date())->String{
164+
letfmt=DateFormatter()
165+
fmt.locale=Locale(identifier:"en_US_POSIX")
166+
fmt.timeZone=TimeZone(secondsFromGMT:0)
167+
fmt.dateFormat="EEE, dd MMM yyyy HH:mm:ss Z"
168+
return fmt.string(from: date)
169+
}
170+
}
171+
172+
enumUpdateChannel:String{case stable, preview}
173+
174+
structRuntimeError:Error,CustomStringConvertible{
175+
varmessage:String
176+
vardescription:String{ message}
177+
init(_ message:String){self.message= message}
178+
}
179+
180+
extensionRegex:@retroactive@uncheckedSendable{}
181+
182+
// Matches CFBundleVersion format: X.Y.Z or X.Y.Z.N
183+
letversionRegex=Regex{
184+
Anchor.startOfLine
185+
Capture{
186+
OneOrMore(.digit)
187+
} transform:{Int($0)!}
188+
"."
189+
Capture{
190+
OneOrMore(.digit)
191+
} transform:{Int($0)!}
192+
"."
193+
Capture{
194+
OneOrMore(.digit)
195+
} transform:{Int($0)!}
196+
Optionally{
197+
Capture{
198+
"."
199+
OneOrMore(.digit)
200+
} transform:{Int($0.dropFirst())!}
201+
}
202+
Anchor.endOfLine
203+
}
204+
205+
letsignatureRegex=Regex{
206+
"sparkle:edSignature=\""
207+
Capture{
208+
OneOrMore(.reluctant){
209+
NegativeLookahead{"\""}
210+
CharacterClass.any
211+
}
212+
} transform:{String($0)}
213+
"\""
214+
OneOrMore(.whitespace)
215+
"length=\""
216+
Capture{
217+
OneOrMore(.digit)
218+
} transform:{Int64($0)}
219+
"\""
220+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp