Skip to content

Some more improvements #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 39 additions & 46 deletions .github/workflows/deploy-api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,43 @@ jobs:
deploy:
name: Build and deploy
runs-on: ubuntu-latest
container:
image: swift:5.7
container: swiftlang/swift:nightly-5.9-jammy@sha256:a3c3ec5e4436c14e44759e38416bb0d46813dc6dd050e787ef3a80b2c3051a36
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build site
run: swift generate-api-docs.swift
- name: Configure AWS Credentials
id: cred
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
aws-region: 'eu-west-2'
- name: Deploy to AWS CloudFormation
id: clouddeploy
uses: aws-actions/[email protected]
with:
name: vapor-api-docs
template: stack.yaml
no-fail-on-empty-changeset: '1'
parameter-overrides: >-
BucketName=vapor-api-docs-site,
SubDomainName=api,
HostedZoneName=vapor.codes,
AcmCertificateArn=${{ secrets.API_DOCS_CERTIFICATE_ARN }}
if: steps.cred.outcome == 'success'
- name: Deploy to S3
id: s3deploy
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read --follow-symlinks
env:
AWS_S3_BUCKET: 'vapor-api-docs-site'
AWS_ACCESS_KEY_ID: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-2'
SOURCE_DIR: 'public'
if: steps.clouddeploy.outcome == 'success'
- name: Invalidate CloudFront
uses: awact/cloudfront-action@master
env:
SOURCE_PATH: '/*'
AWS_ACCESS_KEY_ID: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-2'
DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }}
- name: Checkout
uses: actions/checkout@v3
- name: Build site
run: swift generate-api-docs.swift
- name: Install curl and awscliv2
run: |
apt-get update && apt-get upgrade -y && apt-get install -y curl
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.API_DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.API_DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
aws-region: 'eu-west-2'
- name: Deploy to AWS CloudFormation
uses: aws-actions/aws-cloudformation-github-deploy@v1
with:
name: vapor-api-docs
template: stack.yaml
no-fail-on-empty-changeset: '1'
parameter-overrides: >-
BucketName=vapor-api-docs-site,
SubDomainName=api,
HostedZoneName=vapor.codes,
AcmCertificateArn=${{ secrets.API_DOCS_CERTIFICATE_ARN }}
- name: Deploy to S3 and invalidate CloudFront
env:
DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }}
run: |
aws --no-cli-pager s3 sync \
./public s3://vapor-api-docs-site \
--no-progress \
--acl public-read
aws --no-cli-pager cloudfront create-invalidation \
--distribution-id ${DISTRIBUTION_ID} \
--paths '/*'
91 changes: 17 additions & 74 deletions generate-api-docs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,84 +39,27 @@ let packages: [String: [String]] = [
"apns": ["VaporAPNS"],
]

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

for (package, modules) in packages {
for module in modules {
allModules.append((package: package, module: module))
}
}

let sortedModules = allModules.sorted { $0.module < $1.module }
for object in sortedModules {
let module = object.module
optionsString += "<option value=\"/\(module.lowercased())/documentation/\(module.lowercased())\">\(module)</option>\n"
}

htmlString = htmlString.replacingOccurrences(of: "{{Options}}", with: optionsString)

try shell("mkdir", "public")
try htmlString.write(toFile: "public/index.html", atomically: true, encoding: .utf8)
try shell("cp", "api-docs.png", "public/api-docs.png")
try shell("cp", "error.html", "public/error.html")

// MARK: Functions
@discardableResult
func shell(_ args: String..., returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> Pipe {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
let pipe = Pipe()
if returnStdOut {
task.standardOutput = pipe
}
if let stdIn = stdIn {
task.standardInput = stdIn
}
try task.run()
task.waitUntilExit()
guard task.terminationStatus == 0 else {
throw ShellError(terminationStatus: task.terminationStatus)
}
return pipe
}
let publicDirUrl = URL(fileURLWithPath: "./public", isDirectory: true)
try FileManager.default.removeItem(at: publicDirUrl)
try FileManager.default.createDirectory(at: publicDirUrl, withIntermediateDirectories: true)

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

extension Pipe {
func string() throws -> String? {
let data = try self.fileHandleForReading.readToEnd()!
let result: String?
if let string = String(
data: data,
encoding: String.Encoding.utf8
) {
result = string
} else {
result = nil
}
return result
}
}
try htmlIndex.write(to: publicDirUrl.appendingPathComponent("index.html", isDirectory: false), atomically: true, encoding: .utf8)
try FileManager.copyItem(at: URL(fileURLWithPath: "./api-docs.png", isDirectory: false), into: publicDirUrl)
try FileManager.copyItem(at: URL(fileURLWithPath: "./error.html", isDirectory: false), into: publicDirUrl)

extension FileManager {
func copyItemIfPossible(atPath: String, toPath: String) throws {
var isDirectory: ObjCBool = false
guard self.fileExists(
atPath: toPath,
isDirectory: &isDirectory
) == false else {
return
}
return try self.copyItem(
atPath: atPath,
toPath: toPath
)
func copyItem(at src: URL, into dst: URL) throws {
assert(dst.hasDirectoryPath)

let dstItem = dst.appendingPathComponent(src.lastPathComponent, isDirectory: src.hasDirectoryPath)
try self.copyItem(at: src, to: dstItem)
}
}
62 changes: 45 additions & 17 deletions generate-package-api-docs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ let moduleList = CommandLine.arguments[2]

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

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

try run()

Expand All @@ -34,7 +33,7 @@ func run() throws {
}

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

print("📐 Copying theme")
do {
try FileManager.default.copyItemIfExistsWithoutOverwrite(
at: currentDirectoryUrl.appendingPathComponent("theme-settings.json", isDirectory: false),
to: doccCatalogUrl.appendingPathComponent("theme-settings.json", isDirectory: false)
)
} catch CocoaError.fileReadNoSuchFile, CocoaError.fileWriteFileExists {
// ignore
}
try FileManager.default.copyItemIfExistsWithoutOverwrite(
at: URL.currentDirectory().appending(component: "theme-settings.json"),
to: doccCatalogUrl.appending(component: "theme-settings.json")
)

print("📝 Generating docs")
try shell([
Expand All @@ -92,7 +87,7 @@ func generateDocs(module: String) throws {
"--fallback-bundle-version", "1.0.0",
"--transform-for-static-hosting",
"--hosting-base-path", "/\(module.lowercased())",
"--output-path", publicDirectoryUrl.appendingPathComponent(module.lowercased(), isDirectory: true).path,
"--output-path", publicDirectoryUrl.appending(component: "\(module.lowercased())/").path,
])
}

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

// Run the command:
let task = try Process.run(URL(fileURLWithPath: "/usr/bin/env", isDirectory: false), arguments: args)
let task = try Process.run(URL(filePath: "/usr/bin/env"), arguments: args)
task.waitUntilExit()
guard task.terminationStatus == 0 else {
throw ShellError(terminationStatus: task.terminationStatus)
Expand All @@ -126,18 +121,51 @@ extension FileManager {
func removeItemIfExists(at url: URL) throws {
do {
try self.removeItem(at: url)
} catch let error as NSError where error.domain == CocoaError.errorDomain && error.code == CocoaError.fileNoSuchFile.rawValue {
} catch let error as NSError where error.isCocoaError(.fileNoSuchFile) {
// ignore
}
}

func copyItemIfExistsWithoutOverwrite(at src: URL, to dst: URL) throws {
do {
// https://github.com/apple/swift-corelibs-foundation/pull/4808
#if !canImport(Darwin)
do {
_ = try dst.checkResourceIsReachable()
throw NSError(domain: CocoaError.errorDomain, code: CocoaError.fileWriteFileExists.rawValue)
} catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {}
#endif
try self.copyItem(at: src, to: dst)
} catch let error as NSError where error.domain == CocoaError.errorDomain && error.code == CocoaError.fileReadNoSuchFile.rawValue {
} catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {
// ignore
} catch let error as NSError where error.domain == CocoaError.errorDomain && error.code == CocoaError.fileWriteFileExists.rawValue {
} catch let error as NSError where error.isCocoaError(.fileWriteFileExists) {
// ignore
}
}
}

extension NSError {
func isCocoaError(_ code: CocoaError.Code) -> Bool {
self.domain == CocoaError.errorDomain && self.code == code.rawValue
}
}

#if !canImport(Darwin)
extension URL {
public enum DirectoryHint: Equatable { case isDirectory, notDirectory, inferFromPath }
static func isDirFlag(_ path: some StringProtocol, _ hint: DirectoryHint) -> Bool {
hint == .inferFromPath ? path.last == "/" : hint == .isDirectory
}
public init(filePath: String, directoryHint hint: DirectoryHint = .inferFromPath, relativeTo base: URL? = nil) {
self = URL(fileURLWithPath: filePath, isDirectory: Self.isDirFlag(path, hint), relativeTo: base)
}
public func appending(component: some StringProtocol, directoryHint hint: DirectoryHint = .inferFromPath) -> URL {
self.appendingPathComponent(component, isDirectory: Self.isDirFlag(component, hint))
}
public func appending(components: (some StringProtocol)..., directoryHint hint: DirectoryHint = .inferFromPath) -> URL {
components.dropLast().reduce(self) { $0.appending(component: $1, directoryHint: .isDirectory) }
.appending(component: components.last!, directoryHint: hint)
}
public static func currentDirectory() -> URL { .init(filePath: FileManager.default.currentDirectoryPath, directoryHint: .isDirectory) }
}
#endif
1 change: 1 addition & 0 deletions images/article.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions images/collection.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions images/curly-brackets.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion theme-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
"dark": "rgb(20, 20, 22)",
"light": "rgb(255, 255, 255)"
},
"documentation-intro-fill": "radial-gradient(circle at top, #ccc 15%, #111 100%)"
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-documentation-intro-accent) 15%, rgb(17, 17, 17) 100%)",
"documentation-intro-accent": "rgb(204, 204, 204)"
},
"icons": {
"article": "/images/article.svg",
"collection": "/images/collection.svg",
"curly-brackets": "/images/curly-brackets.svg"
}
},
"features": {
Expand Down