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

Commit0a75509

Browse files
committed
ci: add update-appcast script
1 parent5785fae commit0a75509

File tree

5 files changed

+226
-2
lines changed

5 files changed

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp