Skip to content

Commit 6c0c366

Browse files
authored
Some more improvements (#28)
* Add some icons * Pretty up generate-package-api-docs.swift a bit * Work around swiftlang/swift-corelibs-foundation#4808
1 parent cf8cc36 commit 6c0c366

7 files changed

+116
-138
lines changed

.github/workflows/deploy-api-docs.yml

+39-46
Original file line numberDiff line numberDiff line change
@@ -8,50 +8,43 @@ jobs:
88
deploy:
99
name: Build and deploy
1010
runs-on: ubuntu-latest
11-
container:
12-
image: swift:5.7
11+
container: swiftlang/swift:nightly-5.9-jammy@sha256:a3c3ec5e4436c14e44759e38416bb0d46813dc6dd050e787ef3a80b2c3051a36
1312
steps:
14-
- name: Checkout
15-
uses: actions/checkout@v3
16-
- name: Build site
17-
run: swift generate-api-docs.swift
18-
- name: Configure AWS Credentials
19-
id: cred
20-
uses: aws-actions/configure-aws-credentials@v1
21-
with:
22-
aws-access-key-id: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
23-
aws-secret-access-key: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
24-
aws-region: 'eu-west-2'
25-
- name: Deploy to AWS CloudFormation
26-
id: clouddeploy
27-
uses: aws-actions/[email protected]
28-
with:
29-
name: vapor-api-docs
30-
template: stack.yaml
31-
no-fail-on-empty-changeset: '1'
32-
parameter-overrides: >-
33-
BucketName=vapor-api-docs-site,
34-
SubDomainName=api,
35-
HostedZoneName=vapor.codes,
36-
AcmCertificateArn=${{ secrets.API_DOCS_CERTIFICATE_ARN }}
37-
if: steps.cred.outcome == 'success'
38-
- name: Deploy to S3
39-
id: s3deploy
40-
uses: jakejarvis/s3-sync-action@master
41-
with:
42-
args: --acl public-read --follow-symlinks
43-
env:
44-
AWS_S3_BUCKET: 'vapor-api-docs-site'
45-
AWS_ACCESS_KEY_ID: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
46-
AWS_SECRET_ACCESS_KEY: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
47-
AWS_REGION: 'eu-west-2'
48-
SOURCE_DIR: 'public'
49-
if: steps.clouddeploy.outcome == 'success'
50-
- name: Invalidate CloudFront
51-
uses: awact/cloudfront-action@master
52-
env:
53-
SOURCE_PATH: '/*'
54-
AWS_ACCESS_KEY_ID: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
55-
AWS_SECRET_ACCESS_KEY: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
56-
AWS_REGION: 'eu-west-2'
57-
DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }}
13+
- name: Checkout
14+
uses: actions/checkout@v3
15+
- name: Build site
16+
run: swift generate-api-docs.swift
17+
- name: Install curl and awscliv2
18+
run: |
19+
apt-get update && apt-get upgrade -y && apt-get install -y curl
20+
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
21+
unzip awscliv2.zip
22+
./aws/install
23+
- name: Configure AWS Credentials
24+
uses: aws-actions/configure-aws-credentials@v2
25+
with:
26+
aws-access-key-id: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
27+
aws-secret-access-key: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
28+
aws-region: 'eu-west-2'
29+
- name: Deploy to AWS CloudFormation
30+
uses: aws-actions/aws-cloudformation-github-deploy@v1
31+
with:
32+
name: vapor-api-docs
33+
template: stack.yaml
34+
no-fail-on-empty-changeset: '1'
35+
parameter-overrides: >-
36+
BucketName=vapor-api-docs-site,
37+
SubDomainName=api,
38+
HostedZoneName=vapor.codes,
39+
AcmCertificateArn=${{ secrets.API_DOCS_CERTIFICATE_ARN }}
40+
- name: Deploy to S3 and invalidate CloudFront
41+
env:
42+
DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }}
43+
run: |
44+
aws --no-cli-pager s3 sync \
45+
./public s3://vapor-api-docs-site \
46+
--no-progress \
47+
--acl public-read
48+
aws --no-cli-pager cloudfront create-invalidation \
49+
--distribution-id ${DISTRIBUTION_ID} \
50+
--paths '/*'

generate-api-docs.swift

+17-74
Original file line numberDiff line numberDiff line change
@@ -39,84 +39,27 @@ let packages: [String: [String]] = [
3939
"apns": ["VaporAPNS"],
4040
]
4141

42-
try shell("rm", "-rf", "public/")
43-
let url = URL(fileURLWithPath: "index.html")
44-
var htmlString = try String(contentsOf: url)
45-
var optionsString = ""
46-
var allModules: [(package: String, module: String)] = []
42+
let htmlMenu = packages.values.flatMap { $0 }
43+
.sorted()
44+
.map { "<option value=\"\($0.lowercased())/documentation/\($0.lowercased())\">\($0)</option>" }
45+
.joined(separator: "\n")
4746

48-
for (package, modules) in packages {
49-
for module in modules {
50-
allModules.append((package: package, module: module))
51-
}
52-
}
53-
54-
let sortedModules = allModules.sorted { $0.module < $1.module }
55-
for object in sortedModules {
56-
let module = object.module
57-
optionsString += "<option value=\"/\(module.lowercased())/documentation/\(module.lowercased())\">\(module)</option>\n"
58-
}
59-
60-
htmlString = htmlString.replacingOccurrences(of: "{{Options}}", with: optionsString)
61-
62-
try shell("mkdir", "public")
63-
try htmlString.write(toFile: "public/index.html", atomically: true, encoding: .utf8)
64-
try shell("cp", "api-docs.png", "public/api-docs.png")
65-
try shell("cp", "error.html", "public/error.html")
66-
67-
// MARK: Functions
68-
@discardableResult
69-
func shell(_ args: String..., returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> Pipe {
70-
let task = Process()
71-
task.launchPath = "/usr/bin/env"
72-
task.arguments = args
73-
let pipe = Pipe()
74-
if returnStdOut {
75-
task.standardOutput = pipe
76-
}
77-
if let stdIn = stdIn {
78-
task.standardInput = stdIn
79-
}
80-
try task.run()
81-
task.waitUntilExit()
82-
guard task.terminationStatus == 0 else {
83-
throw ShellError(terminationStatus: task.terminationStatus)
84-
}
85-
return pipe
86-
}
47+
let publicDirUrl = URL(fileURLWithPath: "./public", isDirectory: true)
48+
try FileManager.default.removeItem(at: publicDirUrl)
49+
try FileManager.default.createDirectory(at: publicDirUrl, withIntermediateDirectories: true)
8750

88-
struct ShellError: Error {
89-
var terminationStatus: Int32
90-
}
51+
var htmlIndex = try String(contentsOf: URL(fileURLWithPath: "./index.html", isDirectory: false), encoding: .utf8)
52+
htmlIndex.replace("{{Options}}", with: "\(htmlMenu)\n", maxReplacements: 1)
9153

92-
extension Pipe {
93-
func string() throws -> String? {
94-
let data = try self.fileHandleForReading.readToEnd()!
95-
let result: String?
96-
if let string = String(
97-
data: data,
98-
encoding: String.Encoding.utf8
99-
) {
100-
result = string
101-
} else {
102-
result = nil
103-
}
104-
return result
105-
}
106-
}
54+
try htmlIndex.write(to: publicDirUrl.appendingPathComponent("index.html", isDirectory: false), atomically: true, encoding: .utf8)
55+
try FileManager.copyItem(at: URL(fileURLWithPath: "./api-docs.png", isDirectory: false), into: publicDirUrl)
56+
try FileManager.copyItem(at: URL(fileURLWithPath: "./error.html", isDirectory: false), into: publicDirUrl)
10757

10858
extension FileManager {
109-
func copyItemIfPossible(atPath: String, toPath: String) throws {
110-
var isDirectory: ObjCBool = false
111-
guard self.fileExists(
112-
atPath: toPath,
113-
isDirectory: &isDirectory
114-
) == false else {
115-
return
116-
}
117-
return try self.copyItem(
118-
atPath: atPath,
119-
toPath: toPath
120-
)
59+
func copyItem(at src: URL, into dst: URL) throws {
60+
assert(dst.hasDirectoryPath)
61+
62+
let dstItem = dst.appendingPathComponent(src.lastPathComponent, isDirectory: src.hasDirectoryPath)
63+
try self.copyItem(at: src, to: dstItem)
12164
}
12265
}

generate-package-api-docs.swift

+45-17
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ let moduleList = CommandLine.arguments[2]
1010

1111
let modules = moduleList.components(separatedBy: ",")
1212

13-
let currentDirectoryUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)
14-
let publicDirectoryUrl = currentDirectoryUrl.appendingPathComponent("public", isDirectory: true)
13+
let publicDirectoryUrl = URL.currentDirectory().appending(component: "public/")
1514

1615
try run()
1716

@@ -34,7 +33,7 @@ func run() throws {
3433
}
3534

3635
func ensurePluginAvailable() throws {
37-
let manifestUrl = currentDirectoryUrl.appendingPathComponent("Package.swift", isDirectory: false)
36+
let manifestUrl = URL.currentDirectory().appending(component: "Package.swift")
3837
var manifestContents = try String(contentsOf: manifestUrl, encoding: .utf8)
3938
if !manifestContents.contains(".package(url: \"https://github.com/apple/swift-docc-plugin") {
4039
// This is freely admitted to be quick and dirty. When SE-0301 gets into a release, we can use that.
@@ -54,7 +53,7 @@ func ensurePluginAvailable() throws {
5453
func generateDocs(module: String) throws {
5554
print("🔎 Finding DocC catalog")
5655
let doccCatalogs = try FileManager.default.contentsOfDirectory(
57-
at: currentDirectoryUrl.appendingPathComponent("Sources", isDirectory: true).appendingPathComponent(module, isDirectory: true),
56+
at: URL.currentDirectory().appending(components: "Sources", "\(module)/"),
5857
includingPropertiesForKeys: nil,
5958
options: [.skipsSubdirectoryDescendants]
6059
).filter { $0.hasDirectoryPath && $0.pathExtension == "docc" }
@@ -70,14 +69,10 @@ func generateDocs(module: String) throws {
7069
print("🗂️ Using DocC catalog \(doccCatalogUrl.lastPathComponent)")
7170

7271
print("📐 Copying theme")
73-
do {
74-
try FileManager.default.copyItemIfExistsWithoutOverwrite(
75-
at: currentDirectoryUrl.appendingPathComponent("theme-settings.json", isDirectory: false),
76-
to: doccCatalogUrl.appendingPathComponent("theme-settings.json", isDirectory: false)
77-
)
78-
} catch CocoaError.fileReadNoSuchFile, CocoaError.fileWriteFileExists {
79-
// ignore
80-
}
72+
try FileManager.default.copyItemIfExistsWithoutOverwrite(
73+
at: URL.currentDirectory().appending(component: "theme-settings.json"),
74+
to: doccCatalogUrl.appending(component: "theme-settings.json")
75+
)
8176

8277
print("📝 Generating docs")
8378
try shell([
@@ -92,7 +87,7 @@ func generateDocs(module: String) throws {
9287
"--fallback-bundle-version", "1.0.0",
9388
"--transform-for-static-hosting",
9489
"--hosting-base-path", "/\(module.lowercased())",
95-
"--output-path", publicDirectoryUrl.appendingPathComponent(module.lowercased(), isDirectory: true).path,
90+
"--output-path", publicDirectoryUrl.appending(component: "\(module.lowercased())/").path,
9691
])
9792
}
9893

@@ -111,7 +106,7 @@ func shell(_ args: [String]) throws {
111106
}
112107

113108
// Run the command:
114-
let task = try Process.run(URL(fileURLWithPath: "/usr/bin/env", isDirectory: false), arguments: args)
109+
let task = try Process.run(URL(filePath: "/usr/bin/env"), arguments: args)
115110
task.waitUntilExit()
116111
guard task.terminationStatus == 0 else {
117112
throw ShellError(terminationStatus: task.terminationStatus)
@@ -126,18 +121,51 @@ extension FileManager {
126121
func removeItemIfExists(at url: URL) throws {
127122
do {
128123
try self.removeItem(at: url)
129-
} catch let error as NSError where error.domain == CocoaError.errorDomain && error.code == CocoaError.fileNoSuchFile.rawValue {
124+
} catch let error as NSError where error.isCocoaError(.fileNoSuchFile) {
130125
// ignore
131126
}
132127
}
133128

134129
func copyItemIfExistsWithoutOverwrite(at src: URL, to dst: URL) throws {
135130
do {
131+
// https://github.com/apple/swift-corelibs-foundation/pull/4808
132+
#if !canImport(Darwin)
133+
do {
134+
_ = try dst.checkResourceIsReachable()
135+
throw NSError(domain: CocoaError.errorDomain, code: CocoaError.fileWriteFileExists.rawValue)
136+
} catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {}
137+
#endif
136138
try self.copyItem(at: src, to: dst)
137-
} catch let error as NSError where error.domain == CocoaError.errorDomain && error.code == CocoaError.fileReadNoSuchFile.rawValue {
139+
} catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {
138140
// ignore
139-
} catch let error as NSError where error.domain == CocoaError.errorDomain && error.code == CocoaError.fileWriteFileExists.rawValue {
141+
} catch let error as NSError where error.isCocoaError(.fileWriteFileExists) {
140142
// ignore
141143
}
142144
}
143145
}
146+
147+
extension NSError {
148+
func isCocoaError(_ code: CocoaError.Code) -> Bool {
149+
self.domain == CocoaError.errorDomain && self.code == code.rawValue
150+
}
151+
}
152+
153+
#if !canImport(Darwin)
154+
extension URL {
155+
public enum DirectoryHint: Equatable { case isDirectory, notDirectory, inferFromPath }
156+
static func isDirFlag(_ path: some StringProtocol, _ hint: DirectoryHint) -> Bool {
157+
hint == .inferFromPath ? path.last == "/" : hint == .isDirectory
158+
}
159+
public init(filePath: String, directoryHint hint: DirectoryHint = .inferFromPath, relativeTo base: URL? = nil) {
160+
self = URL(fileURLWithPath: filePath, isDirectory: Self.isDirFlag(path, hint), relativeTo: base)
161+
}
162+
public func appending(component: some StringProtocol, directoryHint hint: DirectoryHint = .inferFromPath) -> URL {
163+
self.appendingPathComponent(component, isDirectory: Self.isDirFlag(component, hint))
164+
}
165+
public func appending(components: (some StringProtocol)..., directoryHint hint: DirectoryHint = .inferFromPath) -> URL {
166+
components.dropLast().reduce(self) { $0.appending(component: $1, directoryHint: .isDirectory) }
167+
.appending(component: components.last!, directoryHint: hint)
168+
}
169+
public static func currentDirectory() -> URL { .init(filePath: FileManager.default.currentDirectoryPath, directoryHint: .isDirectory) }
170+
}
171+
#endif

images/article.svg

+1
Loading

images/collection.svg

+4
Loading

images/curly-brackets.svg

+3
Loading

theme-settings.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@
2121
"dark": "rgb(20, 20, 22)",
2222
"light": "rgb(255, 255, 255)"
2323
},
24-
"documentation-intro-fill": "radial-gradient(circle at top, #ccc 15%, #111 100%)"
24+
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-documentation-intro-accent) 15%, rgb(17, 17, 17) 100%)",
25+
"documentation-intro-accent": "rgb(204, 204, 204)"
26+
},
27+
"icons": {
28+
"article": "/images/article.svg",
29+
"collection": "/images/collection.svg",
30+
"curly-brackets": "/images/curly-brackets.svg"
2531
}
2632
},
2733
"features": {

0 commit comments

Comments
 (0)