CI: Remove documentation in favor of notion.so
This also means we no longer need the publish to pub.dev script because it just removes the docs as a workaround and does nothing more.
This commit is contained in:
parent
b4c922f49c
commit
f36299c3d7
|
|
@ -98,37 +98,6 @@ code_quality:
|
||||||
paths:
|
paths:
|
||||||
- code-quality-report.json
|
- code-quality-report.json
|
||||||
|
|
||||||
build_doc:
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
stage: builddocs
|
|
||||||
image: registry.gitlab.com/larodar/mdbook-dtmo:latest
|
|
||||||
script:
|
|
||||||
- cd docs
|
|
||||||
- mdbook-dtmo build -d public
|
|
||||||
- mv public ../doc-public
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- doc-public
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
pages:
|
|
||||||
tags:
|
|
||||||
- linux
|
|
||||||
stage: deploy
|
|
||||||
image: alpine:latest
|
|
||||||
script:
|
|
||||||
- mv doc-public ./home/doc
|
|
||||||
- mv home public
|
|
||||||
dependencies:
|
|
||||||
- build_doc
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
|
|
||||||
dry-run:
|
dry-run:
|
||||||
stage: publish
|
stage: publish
|
||||||
image: google/dart
|
image: google/dart
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
[book]
|
|
||||||
title = "Famedly Matrix SDK Documentation"
|
|
||||||
description = "Famedly Matrix SDK Documentation"
|
|
||||||
|
|
||||||
[preprocessor.mermaid]
|
|
||||||
command = "mdbook-mermaid"
|
|
||||||
renderer = ["html"]
|
|
||||||
|
|
||||||
[preprocessor.toc]
|
|
||||||
command = "mdbook-toc"
|
|
||||||
|
|
||||||
[output.html]
|
|
||||||
additional-css = ["mermaid.css"]
|
|
||||||
additional-js = ["mermaid.min.js", "mermaid-init.js"]
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
mermaid.initialize({startOnLoad:true});
|
|
||||||
351
docs/mermaid.css
351
docs/mermaid.css
|
|
@ -1,351 +0,0 @@
|
||||||
/* Flowchart variables */
|
|
||||||
/* Sequence Diagram variables */
|
|
||||||
/* Gantt chart variables */
|
|
||||||
.mermaid .mermaid .label {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.mermaid .node rect,
|
|
||||||
.mermaid .node circle,
|
|
||||||
.mermaid .node ellipse,
|
|
||||||
.mermaid .node polygon {
|
|
||||||
fill: #ECECFF;
|
|
||||||
stroke: #CCCCFF;
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
.mermaid .arrowheadPath {
|
|
||||||
fill: #333333;
|
|
||||||
}
|
|
||||||
.mermaid .edgePath .path {
|
|
||||||
stroke: #333333;
|
|
||||||
}
|
|
||||||
.mermaid .edgeLabel {
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
}
|
|
||||||
.mermaid .cluster rect {
|
|
||||||
fill: #ffffde !important;
|
|
||||||
rx: 4 !important;
|
|
||||||
stroke: #aaaa33 !important;
|
|
||||||
stroke-width: 1px !important;
|
|
||||||
}
|
|
||||||
.mermaid .cluster text {
|
|
||||||
fill: #333;
|
|
||||||
}
|
|
||||||
.mermaid .actor {
|
|
||||||
stroke: #CCCCFF;
|
|
||||||
fill: #ECECFF;
|
|
||||||
}
|
|
||||||
.mermaid text.actor {
|
|
||||||
fill: black;
|
|
||||||
stroke: none;
|
|
||||||
}
|
|
||||||
.mermaid .actor-line {
|
|
||||||
stroke: grey;
|
|
||||||
}
|
|
||||||
.mermaid .messageLine0 {
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-dasharray: "2 2";
|
|
||||||
marker-end: "url(#arrowhead)";
|
|
||||||
stroke: #333;
|
|
||||||
}
|
|
||||||
.mermaid .messageLine1 {
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-dasharray: "2 2";
|
|
||||||
stroke: #333;
|
|
||||||
}
|
|
||||||
.mermaid #arrowhead {
|
|
||||||
fill: #333;
|
|
||||||
}
|
|
||||||
.mermaid #crosshead path {
|
|
||||||
fill: #333 !important;
|
|
||||||
stroke: #333 !important;
|
|
||||||
}
|
|
||||||
.mermaid .messageText {
|
|
||||||
fill: #333;
|
|
||||||
stroke: none;
|
|
||||||
}
|
|
||||||
.mermaid .labelBox {
|
|
||||||
stroke: #CCCCFF;
|
|
||||||
fill: #ECECFF;
|
|
||||||
}
|
|
||||||
.mermaid .labelText {
|
|
||||||
fill: black;
|
|
||||||
stroke: none;
|
|
||||||
}
|
|
||||||
.mermaid .loopText {
|
|
||||||
fill: black;
|
|
||||||
stroke: none;
|
|
||||||
}
|
|
||||||
.mermaid .loopLine {
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-dasharray: "2 2";
|
|
||||||
marker-end: "url(#arrowhead)";
|
|
||||||
stroke: #CCCCFF;
|
|
||||||
}
|
|
||||||
.mermaid .note {
|
|
||||||
stroke: #aaaa33;
|
|
||||||
fill: #fff5ad;
|
|
||||||
}
|
|
||||||
.mermaid .noteText {
|
|
||||||
fill: black;
|
|
||||||
stroke: none;
|
|
||||||
font-family: 'trebuchet ms', verdana, arial;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
/** Section styling */
|
|
||||||
.mermaid .section {
|
|
||||||
stroke: none;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
.mermaid .section0 {
|
|
||||||
fill: rgba(102, 102, 255, 0.49);
|
|
||||||
}
|
|
||||||
.mermaid .section2 {
|
|
||||||
fill: #fff400;
|
|
||||||
}
|
|
||||||
.mermaid .section1,
|
|
||||||
.mermaid .section3 {
|
|
||||||
fill: white;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
.mermaid .sectionTitle0 {
|
|
||||||
fill: #333;
|
|
||||||
}
|
|
||||||
.mermaid .sectionTitle1 {
|
|
||||||
fill: #333;
|
|
||||||
}
|
|
||||||
.mermaid .sectionTitle2 {
|
|
||||||
fill: #333;
|
|
||||||
}
|
|
||||||
.mermaid .sectionTitle3 {
|
|
||||||
fill: #333;
|
|
||||||
}
|
|
||||||
.mermaid .sectionTitle {
|
|
||||||
text-anchor: start;
|
|
||||||
font-size: 11px;
|
|
||||||
text-height: 14px;
|
|
||||||
}
|
|
||||||
/* Grid and axis */
|
|
||||||
.mermaid .grid .tick {
|
|
||||||
stroke: lightgrey;
|
|
||||||
opacity: 0.3;
|
|
||||||
shape-rendering: crispEdges;
|
|
||||||
}
|
|
||||||
.mermaid .grid path {
|
|
||||||
stroke-width: 0;
|
|
||||||
}
|
|
||||||
/* Today line */
|
|
||||||
.mermaid .today {
|
|
||||||
fill: none;
|
|
||||||
stroke: red;
|
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
/* Task styling */
|
|
||||||
/* Default task */
|
|
||||||
.mermaid .task {
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
.mermaid .taskText {
|
|
||||||
text-anchor: middle;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.mermaid .taskTextOutsideRight {
|
|
||||||
fill: black;
|
|
||||||
text-anchor: start;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.mermaid .taskTextOutsideLeft {
|
|
||||||
fill: black;
|
|
||||||
text-anchor: end;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
/* Specific task settings for the sections*/
|
|
||||||
.mermaid .taskText0,
|
|
||||||
.mermaid .taskText1,
|
|
||||||
.mermaid .taskText2,
|
|
||||||
.mermaid .taskText3 {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
.mermaid .task0,
|
|
||||||
.mermaid .task1,
|
|
||||||
.mermaid .task2,
|
|
||||||
.mermaid .task3 {
|
|
||||||
fill: #8a90dd;
|
|
||||||
stroke: #534fbc;
|
|
||||||
}
|
|
||||||
.mermaid .taskTextOutside0,
|
|
||||||
.mermaid .taskTextOutside2 {
|
|
||||||
fill: black;
|
|
||||||
}
|
|
||||||
.mermaid .taskTextOutside1,
|
|
||||||
.mermaid .taskTextOutside3 {
|
|
||||||
fill: black;
|
|
||||||
}
|
|
||||||
/* Active task */
|
|
||||||
.mermaid .active0,
|
|
||||||
.mermaid .active1,
|
|
||||||
.mermaid .active2,
|
|
||||||
.mermaid .active3 {
|
|
||||||
fill: #bfc7ff;
|
|
||||||
stroke: #534fbc;
|
|
||||||
}
|
|
||||||
.mermaid .activeText0,
|
|
||||||
.mermaid .activeText1,
|
|
||||||
.mermaid .activeText2,
|
|
||||||
.mermaid .activeText3 {
|
|
||||||
fill: black !important;
|
|
||||||
}
|
|
||||||
/* Completed task */
|
|
||||||
.mermaid .done0,
|
|
||||||
.mermaid .done1,
|
|
||||||
.mermaid .done2,
|
|
||||||
.mermaid .done3 {
|
|
||||||
stroke: grey;
|
|
||||||
fill: lightgrey;
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
.mermaid .doneText0,
|
|
||||||
.mermaid .doneText1,
|
|
||||||
.mermaid .doneText2,
|
|
||||||
.mermaid .doneText3 {
|
|
||||||
fill: black !important;
|
|
||||||
}
|
|
||||||
/* Tasks on the critical line */
|
|
||||||
.mermaid .crit0,
|
|
||||||
.mermaid .crit1,
|
|
||||||
.mermaid .crit2,
|
|
||||||
.mermaid .crit3 {
|
|
||||||
stroke: #ff8888;
|
|
||||||
fill: red;
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
.mermaid .activeCrit0,
|
|
||||||
.mermaid .activeCrit1,
|
|
||||||
.mermaid .activeCrit2,
|
|
||||||
.mermaid .activeCrit3 {
|
|
||||||
stroke: #ff8888;
|
|
||||||
fill: #bfc7ff;
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
.mermaid .doneCrit0,
|
|
||||||
.mermaid .doneCrit1,
|
|
||||||
.mermaid .doneCrit2,
|
|
||||||
.mermaid .doneCrit3 {
|
|
||||||
stroke: #ff8888;
|
|
||||||
fill: lightgrey;
|
|
||||||
stroke-width: 2;
|
|
||||||
cursor: pointer;
|
|
||||||
shape-rendering: crispEdges;
|
|
||||||
}
|
|
||||||
.mermaid .doneCritText0,
|
|
||||||
.mermaid .doneCritText1,
|
|
||||||
.mermaid .doneCritText2,
|
|
||||||
.mermaid .doneCritText3 {
|
|
||||||
fill: black !important;
|
|
||||||
}
|
|
||||||
.mermaid .activeCritText0,
|
|
||||||
.mermaid .activeCritText1,
|
|
||||||
.mermaid .activeCritText2,
|
|
||||||
.mermaid .activeCritText3 {
|
|
||||||
fill: black !important;
|
|
||||||
}
|
|
||||||
.mermaid .titleText {
|
|
||||||
text-anchor: middle;
|
|
||||||
font-size: 18px;
|
|
||||||
fill: black;
|
|
||||||
}
|
|
||||||
.mermaid g.classGroup text {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: none;
|
|
||||||
font-family: 'trebuchet ms', verdana, arial;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.mermaid g.classGroup rect {
|
|
||||||
fill: #ECECFF;
|
|
||||||
stroke: #9370DB;
|
|
||||||
}
|
|
||||||
.mermaid g.classGroup line {
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid svg .classLabel .box {
|
|
||||||
stroke: none;
|
|
||||||
stroke-width: 0;
|
|
||||||
fill: #ECECFF;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
.mermaid svg .classLabel .label {
|
|
||||||
fill: #9370DB;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.mermaid .relation {
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
.mermaid .composition {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #compositionStart {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #compositionEnd {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid .aggregation {
|
|
||||||
fill: #ECECFF;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #aggregationStart {
|
|
||||||
fill: #ECECFF;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #aggregationEnd {
|
|
||||||
fill: #ECECFF;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #dependencyStart {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #dependencyEnd {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #extensionStart {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid #extensionEnd {
|
|
||||||
fill: #9370DB;
|
|
||||||
stroke: #9370DB;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.mermaid .node text {
|
|
||||||
font-family: 'trebuchet ms', verdana, arial;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.mermaid div.mermaidTooltip {
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 200px;
|
|
||||||
padding: 2px;
|
|
||||||
font-family: 'trebuchet ms', verdana, arial;
|
|
||||||
font-size: 12px;
|
|
||||||
background: #ffffde;
|
|
||||||
border: 1px solid #aaaa33;
|
|
||||||
border-radius: 2px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +0,0 @@
|
||||||
[About](readme.md)
|
|
||||||
[End-to-end encryption](e2ee.md)
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
# End-to-end encryption
|
|
||||||
|
|
||||||
End-to-end encryption is rather complicated. Beyond the
|
|
||||||
[bare-bones implementation](https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide)
|
|
||||||
and [advanced e2ee features](https://matrix.org/docs/guides/implementing-more-advanced-e-2-ee-features-such-as-cross-signing)
|
|
||||||
a lot of miscellaneous small tricks have been added to make the e2ee experience smoother. This file
|
|
||||||
acts as a list of said tricks, in no particular order.
|
|
||||||
|
|
||||||
## Rotate of megolm sessions
|
|
||||||
Megolm sessions are rotated (cleared) after encrypting 100 messages or one week, whatever happens
|
|
||||||
earlier. Additionally, megolm sessions are rotated if a device leaves the room. If a new device joins
|
|
||||||
the room the megolm session is re-used and it is sent at a later index to that device.
|
|
||||||
|
|
||||||
## Requesting known SSSS secrets
|
|
||||||
Upon new login you can either self-verify and cache SSSS secrets with your recovery passphrase / recovery
|
|
||||||
key to get the cross-signing and megolm backup keys, or you can self-verify via emoji and afterwards
|
|
||||||
requests from other devices, after successful self-verification.
|
|
||||||
|
|
||||||
For SSSS secrets we want to cache (self-signing key, user-signing key, megolm backup key) we automatically
|
|
||||||
request those secrets from other devices after successful self-verification, if we weren't verified
|
|
||||||
before and we don't have them cached.
|
|
||||||
|
|
||||||
Additionally, if we still don't have the secrets cached, we try to intelligently guess if other of
|
|
||||||
our own verified devices are online, max. once per 15 min. This is triggered on receiving `to_device`
|
|
||||||
events from ourself and getting messages down `/sync` from ourself that weren't sent by us.
|
|
||||||
|
|
||||||
## Starting megolm sessions while typing
|
|
||||||
In order to speed up sending of messages in e2ee rooms, megolm sessions are already created and sent
|
|
||||||
while a user is typing in the room. While this in theory can result in a megolm session being used to
|
|
||||||
encrypt zero messages (a device of the room is being removed between typing and sending), in most cases
|
|
||||||
this will increase sending performance.
|
|
||||||
|
|
||||||
## Auto-reply to foreign key requests
|
|
||||||
When sending a megolm session we record to which device at which index we send the megolm session. On
|
|
||||||
key requests from other users, we automatically forward the megolm session at the index noted, as in
|
|
||||||
theory they should have that key anyways. This helps to improve recovery from unable to decrypts.
|
|
||||||
|
|
||||||
## Chunked priority sending of megolm keys
|
|
||||||
In the background we record the last activity time of all devices. This is determined on when we
|
|
||||||
received the last encrypted `to_device` message of that device. (It could be optimized by also including
|
|
||||||
encrypted room events). Now, when creating a megolm session, we sort the device list, and chunk it into
|
|
||||||
chunks of 20. We wait for the first chunk to send, and send the remaining chunks in the background.
|
|
||||||
This way we make sure that the devices active right now get the key for sure right away, and then,
|
|
||||||
prioritized by activity, the next devices get the keys seemlessly in the background.
|
|
||||||
|
|
||||||
As we implemented auto-reply to foreign key requests other devices can already request the key before
|
|
||||||
it got received, also ensuring high-availability in case of a badly sorted list.
|
|
||||||
|
|
||||||
## OTK (One-Time Key) upload and failure
|
|
||||||
Because libolm can only hold up to 100 OTKs at all times, we must not upload 100 OTKs. If we were to
|
|
||||||
do that then another person might claim an OTK and, before they send you a `to_device` message, you'd
|
|
||||||
upload a new OTK to fill up the 100 OTKs again, forgetting the OTK the other person used. So, we try
|
|
||||||
to keep the OTKs uploaded at roughly 2/3, so 66 keys.
|
|
||||||
|
|
||||||
Additionally, we must make sure that we do not lose any OTKs uploaded, even if the upload request
|
|
||||||
failed. So we store the olm account, and thus the OTKs, both before and after requesting. We only
|
|
||||||
mark the OTKs as uploaded after the request was successful.
|
|
||||||
|
|
||||||
If now the upload fails, we already stored the non-uploaded OTKs. Thus, next time when attempting to
|
|
||||||
upload, we take the non-uploaded OTKs into account for how many to create, and then re-try the
|
|
||||||
uploading.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph
|
|
||||||
sync(Sync response says more than half of all OTKs have been used) --> generate(Generate new OTKs, so that we have up to 2/3rd of all full)
|
|
||||||
generate --> store(Store Olm-Account and OTKs in database)
|
|
||||||
store --> upload(Attempt to upload OTKs)
|
|
||||||
upload -- Success --> mark(Mark OTKs as uploaded)
|
|
||||||
mark --> store2(Store Olm-Account and OTKs in database)
|
|
||||||
upload -- Failure --> fail(Don't do anything)
|
|
||||||
fail --> sync
|
|
||||||
```
|
|
||||||
|
|
||||||
## Auto-recreate corrupted olm sessions
|
|
||||||
If we receive an encrypted `to_device` message that we can't decrypt, that means the olm session with
|
|
||||||
the remote device got corrupted. So, we create a new olm session and send an encrypted `m.dummy` via
|
|
||||||
`to_device` messaging to signal the new olm session.
|
|
||||||
|
|
||||||
## Replay of sent `to_device` messages
|
|
||||||
As olm is a double-ratchet the ratchet on the receiving and the sending client must be the same. So,
|
|
||||||
a lost `to_device` event could be fatal to the olm session. Thus, we record all sent `to_device` messages
|
|
||||||
that failed to send. Before sending the next `to_device` message (and periodically after `/sync`) we
|
|
||||||
empty that queue, to make sure that the `to_device` messages are sent, and thus the olm ratchets stay
|
|
||||||
in sync.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph
|
|
||||||
trigger(Trigger to send a to_device message) --> queue(Attempt to re-send all existing to_device messages from the queue)
|
|
||||||
queue -- Failure --> add_queue(Add to_device message to queue)
|
|
||||||
queue -- Success --> remove_queue(Remove sent to_device messages from queue)
|
|
||||||
remove_queue --> send(Attempt to actually send the to_device message)
|
|
||||||
send -- Success --> Done
|
|
||||||
send -- Failure --> add_queue
|
|
||||||
```
|
|
||||||
|
|
||||||
Additionally, when sending an encrypted `to_device` event to a device, we remember that content, one
|
|
||||||
message per recipient device. Now, if we receive an encrypted `m.dummy`, this usually indicates that
|
|
||||||
the remote device started a new olm session, likely due to corruption. So, we re-send the saved
|
|
||||||
content, as it might e.g. contain a megolm key needed to decrypt messages.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Some notes
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh -e
|
|
||||||
mv docs .docs
|
|
||||||
flutter pub publish --dry-run
|
|
||||||
flutter pub publish
|
|
||||||
mv .docs docs
|
|
||||||
Loading…
Reference in New Issue