Compare commits
230 Commits
proj-pytho
...
proj-pytho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a499f8f121 | ||
|
|
c06c32da29 | ||
|
|
9a59327c1b | ||
|
|
23ca0e1d54 | ||
|
|
92f5ee33c8 | ||
|
|
e8778aa251 | ||
|
|
a650bc1cc4 | ||
|
|
b4392a2c87 | ||
|
|
bd951e122a | ||
|
|
87f5d9110e | ||
|
|
21b5a0f317 | ||
|
|
fee01e49e8 | ||
|
|
da29fa2ba3 | ||
|
|
a754bffa65 | ||
|
|
e37c0b9648 | ||
|
|
e97a1ce7ce | ||
|
|
4f0d58df5a | ||
|
|
f45c702be6 | ||
|
|
859625c587 | ||
|
|
26b79493e0 | ||
|
|
bda65e63db | ||
|
|
74d6c3f356 | ||
|
|
815d7a3186 | ||
|
|
92171ab506 | ||
|
|
4fc51375be | ||
|
|
de8a48277f | ||
|
|
dad98c137d | ||
|
|
68d9786697 | ||
|
|
db02774c9c | ||
|
|
3b955d9067 | ||
|
|
6b190fdcb1 | ||
|
|
d3d865dda9 | ||
|
|
558c69ee84 | ||
|
|
86bc79d826 | ||
|
|
6739de6bfc | ||
|
|
f21fda68f1 | ||
|
|
350561daf9 | ||
|
|
cc99a79835 | ||
|
|
8a5c220e5c | ||
|
|
daeb777e4d | ||
|
|
708f4696ca | ||
|
|
fc453f6bbc | ||
|
|
f50d99d709 | ||
|
|
c19e0fa75f | ||
|
|
fe9b74f261 | ||
|
|
3f720c9624 | ||
|
|
0f2d049daf | ||
|
|
3d295d8025 | ||
|
|
456dc422fc | ||
|
|
008c87b572 | ||
|
|
b21d3365b7 | ||
|
|
42b06a759f | ||
|
|
b3088ecfe4 | ||
|
|
a1d8b0650a | ||
|
|
9740c5fe74 | ||
|
|
bfdd0754ed | ||
|
|
df6404e2f5 | ||
|
|
22224b8304 | ||
|
|
11c83cab30 | ||
|
|
ee3c3ff237 | ||
|
|
467734b04e | ||
|
|
e1df9b5c14 | ||
|
|
e1083a25f7 | ||
|
|
fe3ea7401d | ||
|
|
1815a69384 | ||
|
|
af7a73b5f7 | ||
|
|
9035fda953 | ||
|
|
b4df47d8c8 | ||
|
|
4d270c04d1 | ||
|
|
ea56586c04 | ||
|
|
ee1aa7620c | ||
|
|
0c871cbd68 | ||
|
|
8f038a2476 | ||
|
|
52dad58417 | ||
|
|
97bbf28724 | ||
|
|
b06558f7fc | ||
|
|
9e4bb24349 | ||
|
|
2c269334ab | ||
|
|
b594e954d8 | ||
|
|
de545a639e | ||
|
|
cc9f0cc6da | ||
|
|
35588c7f9e | ||
|
|
c5920c39e8 | ||
|
|
1ffcaee719 | ||
|
|
9f619f71cc | ||
|
|
1e63d81675 | ||
|
|
a13415c31f | ||
|
|
22554bed89 | ||
|
|
db1155d958 | ||
|
|
e0b446f108 | ||
|
|
ba51fad9d2 | ||
|
|
d4b7faa3ff | ||
|
|
8c8fba1874 | ||
|
|
ca970b80e7 | ||
|
|
2b9ead20f9 | ||
|
|
f0f9b98f0f | ||
|
|
1cccfd3c66 | ||
|
|
79f2bc3a06 | ||
|
|
6171621ea3 | ||
|
|
f6d0e32a02 | ||
|
|
1697548096 | ||
|
|
32b49c788c | ||
|
|
f447bdaa8f | ||
|
|
fbb2824944 | ||
|
|
34331c5a79 | ||
|
|
935e458161 | ||
|
|
31f5c10ffe | ||
|
|
c77efb4ba1 | ||
|
|
e7510b606e | ||
|
|
ff2dc465f7 | ||
|
|
43de9c4f6a | ||
|
|
126098289b | ||
|
|
fb808245e3 | ||
|
|
159462606f | ||
|
|
7d1ba026db | ||
|
|
18199cf553 | ||
|
|
1fd87edf97 | ||
|
|
76097a2c65 | ||
|
|
cc5eb4332c | ||
|
|
54ad1db5c8 | ||
|
|
a3af25f5b0 | ||
|
|
6ec9182ad0 | ||
|
|
efb033096e | ||
|
|
91bd482969 | ||
|
|
010ab29959 | ||
|
|
3f897aa4f8 | ||
|
|
d17158db77 | ||
|
|
29c80be876 | ||
|
|
76dc608ed8 | ||
|
|
ded4524241 | ||
|
|
65b3027992 | ||
|
|
d72ca3c6cc | ||
|
|
0622b4004a | ||
|
|
8348a94edb | ||
|
|
c96bd66166 | ||
|
|
21a54d7591 | ||
|
|
b8fb092432 | ||
|
|
5f6d3ee14e | ||
|
|
f6b143d489 | ||
|
|
8e5d085924 | ||
|
|
e1b4627f33 | ||
|
|
fd50372b5d | ||
|
|
322edf01ba | ||
|
|
50059a9790 | ||
|
|
1115e30d3b | ||
|
|
518877e2f6 | ||
|
|
b0749f1b2b | ||
|
|
c7cba11e85 | ||
|
|
a1bcd98682 | ||
|
|
95b077f039 | ||
|
|
840a2b71e8 | ||
|
|
8325bb3375 | ||
|
|
00e078cfa4 | ||
|
|
105039ee2f | ||
|
|
35e712e21b | ||
|
|
5003a1e5ca | ||
|
|
949096259a | ||
|
|
4feb3d4230 | ||
|
|
4be8421fae | ||
|
|
652c2a1799 | ||
|
|
668289e248 | ||
|
|
37417f1e1f | ||
|
|
6901a209e2 | ||
|
|
67e37c58b4 | ||
|
|
65e54c1d17 | ||
|
|
57ef35fd81 | ||
|
|
576c774001 | ||
|
|
07c8dfab7c | ||
|
|
fe26b3ba57 | ||
|
|
a7ab68a82a | ||
|
|
8a5b76955d | ||
|
|
dd3e9ffb8d | ||
|
|
c1adff4357 | ||
|
|
b90e090fec | ||
|
|
b8d7c3fe1b | ||
|
|
74efddd5f4 | ||
|
|
945263e788 | ||
|
|
11cf0a20b7 | ||
|
|
04027d1310 | ||
|
|
fdc0cdeb8b | ||
|
|
10d46bb28e | ||
|
|
02afe10001 | ||
|
|
4d8a8e93fb | ||
|
|
7528f2f3b0 | ||
|
|
7755c56386 | ||
|
|
65978ae49a | ||
|
|
7875745bfe | ||
|
|
2f778b9dd9 | ||
|
|
3b3ec3e396 | ||
|
|
fa1946c7d9 | ||
|
|
decf483773 | ||
|
|
0a82070f27 | ||
|
|
21486dd01b | ||
|
|
4ce5db9db3 | ||
|
|
781045b019 | ||
|
|
fa2cbffbd5 | ||
|
|
0695681d7c | ||
|
|
b5a67bf540 | ||
|
|
83744d69a7 | ||
|
|
31650d2f87 | ||
|
|
c19a5dab9e | ||
|
|
b32310dad9 | ||
|
|
e1e553e39c | ||
|
|
b6c062b365 | ||
|
|
22a294c135 | ||
|
|
012c0e5661 | ||
|
|
c7dfb541f3 | ||
|
|
1ea8c670f7 | ||
|
|
7ed9553ee3 | ||
|
|
ba67bef0d2 | ||
|
|
6902bb5020 | ||
|
|
e6cf33c740 | ||
|
|
39daa3258a | ||
|
|
ef60fa5ee5 | ||
|
|
386f8ce2ec | ||
|
|
34c3a5f9e6 | ||
|
|
ce0372fd73 | ||
|
|
6df12e022a | ||
|
|
5e5ff95ef7 | ||
|
|
c8c39cfeb1 | ||
|
|
89056a9d5d | ||
|
|
db4f81cb38 | ||
|
|
2daba8f0b4 | ||
|
|
4e1bb2a25a | ||
|
|
7e9d0e6a2b | ||
|
|
0e9f773d55 | ||
|
|
8ac019cefb | ||
|
|
ac78a328eb | ||
|
|
0e915e0437 | ||
|
|
d23bea5a3a |
@@ -1,7 +1,7 @@
|
||||
;; -*- comment-column: 100; -*-
|
||||
{:config-paths ["macros" "src" "config"]
|
||||
:linters
|
||||
{ ;;
|
||||
{;;
|
||||
;; enabled optional linters (these are disabled by default)
|
||||
;;
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests
|
||||
;; don't include `:h2` since that will probably lead to a mountain of tests where we are specifically testing
|
||||
;; middleware or compilation behavior with just H2 since it's the default.
|
||||
{:drivers #{ ;; core drivers
|
||||
{:drivers #{;; core drivers
|
||||
#_:h2
|
||||
:postgres
|
||||
:mysql
|
||||
@@ -228,7 +228,7 @@
|
||||
(clojure.core.logic/fresh)
|
||||
(clojure.core.logic/matcha)
|
||||
(clojure.core.logic/run)
|
||||
(clojure.test/is [=? malli= qv=])
|
||||
(clojure.test/is [query= =? malli= qv=])
|
||||
(clojure.tools.macro/macrolet)
|
||||
instaparse.core/transform
|
||||
(metabase.server.streaming-response/streaming-response)
|
||||
@@ -418,6 +418,7 @@
|
||||
metabase-enterprise.transforms.api-test/with-transform-tags clojure.core/let
|
||||
metabase-enterprise.transforms.api-test/with-worker-runs clojure.core/let
|
||||
metabase-enterprise.transforms.test-util/with-transform-cleanup! clojure.core/let
|
||||
metabase-enterprise.transforms.execute/with-transform-lifecycle clojure.core/let
|
||||
metabase.actions.test-util/with-actions clojure.core/let
|
||||
metabase.api.common/let-404 clojure.core/let
|
||||
metabase.app-db.custom-migrations.util/with-temp-scheduler! clojure.core/fn
|
||||
@@ -713,18 +714,13 @@
|
||||
{:pattern "^metabase\\.lib\\..*"
|
||||
:name metabase-lib-excluding-legacy-mbql}
|
||||
;;
|
||||
;; QP and driver source namespaces: `metabase.query-processor.*`, `metabase-enterprise.sandbox.query-processor.*`,
|
||||
;; and `metabase.driver.*`, excluding `-test` namespaces.
|
||||
;; QP source namespaces: `metabase.query-processor.*`, `metabase-enterprise.sandbox.query-processor.*`, and
|
||||
;; `metabase.driver.*`, excluding `-test` namespaces.
|
||||
;;
|
||||
{:pattern "metabase(?:(?:(?:-enterprise\\.sandbox)?\\.query-processor\\.)|(?:\\.driver\\.)).*(?<!-test)$"
|
||||
:name qp-and-driver-source-namespaces}
|
||||
;;
|
||||
;; QP source namespaces
|
||||
;;
|
||||
{:pattern "metabase(?:(?:-enterprise\\.sandbox)?\\.query-processor\\.).*(?<!-test)$"
|
||||
:name qp-source-namespaces}
|
||||
;;
|
||||
;; driver namespaces, including tests
|
||||
;; driver namespaces
|
||||
;;
|
||||
{:pattern "metabase\\.driver.*$"
|
||||
:name driver-namespaces}
|
||||
@@ -739,7 +735,9 @@
|
||||
;; `metabase-enterprise.sandbox.query-processor*-test`
|
||||
;;
|
||||
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
|
||||
:name general-qp-and-driver-test-namespaces}]
|
||||
:name general-qp-and-driver-test-namespaces}
|
||||
{:pattern "^metabase\\.python-runner\\..*"
|
||||
:name python-runner-namespaces}]
|
||||
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
@@ -784,7 +782,7 @@
|
||||
clojure.string/lower-case {:level :off}
|
||||
clojure.string/upper-case {:level :off}}}}
|
||||
|
||||
;; enable all the extra linters we haven't enabled in the core app yet in Lib. It's all new code so we can adhere
|
||||
;; enable all the extra linters we haven't enabled in the core app yet in MLv2. It's all new code so we can adhere
|
||||
;; to the new stricter rules from day 1
|
||||
metabase-lib
|
||||
{:linters
|
||||
@@ -804,25 +802,19 @@
|
||||
metabase.test/with-temp {:message "Don't use application database inside Lib code"}}
|
||||
|
||||
:discouraged-namespace
|
||||
{metabase.driver {:message "Lib code should be independent of driver code/methods"}
|
||||
metabase.test {:message "Lib tests should only use Lib test helpers"}
|
||||
metabase.test.data {:message "Lib tests should only use Lib test helpers"}
|
||||
metabase.util.malli.schema {:message "Prefer metabase.lib.schema schemas in Lib code"}
|
||||
toucan.db {:message "Don't use application database inside Lib code"}
|
||||
toucan.models {:message "Don't use application database inside Lib code"}
|
||||
toucan.util.test {:message "Don't use application database inside Lib code"}
|
||||
toucan2.core {:message "Don't use application database inside Lib code"}}}}
|
||||
{metabase.driver {:message "MLv2 code should be independent of driver code/methods"}
|
||||
metabase.test {:message "MLv2 tests should only use MLv2 test helpers"}
|
||||
metabase.test.data {:message "MLv2 tests should only use MLv2 test helpers"}
|
||||
metabase.util.malli.schema {:message "Prefer metabase.lib.schema schemas in MLv2 code"}
|
||||
toucan.db {:message "Don't use application database inside MLv2 code"}
|
||||
toucan.models {:message "Don't use application database inside MLv2 code"}
|
||||
toucan.util.test {:message "Don't use application database inside MLv2 code"}
|
||||
toucan2.core {:message "Don't use application database inside MLv2 code"}}}}
|
||||
|
||||
metabase-lib-excluding-legacy-mbql
|
||||
{:linters
|
||||
{:discouraged-var
|
||||
{metabase.legacy-mbql.util/unique-name-generator {:message "Use metabase.lib.util/unique-name-generator or non-truncating-unique-name-generator"}}}
|
||||
|
||||
:discouraged-namespace
|
||||
{metabase.legacy-mbql.js {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}
|
||||
metabase.legacy-mbql.jvm-util {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}
|
||||
metabase.legacy-mbql.predicates {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}
|
||||
metabase.legacy-mbql.util {:message "Avoid legacy MBQL utils in Lib; move things into Lib if needded."}}}
|
||||
{metabase.legacy-mbql.util/unique-name-generator {:message "Use metabase.lib.util/unique-name-generator or non-truncating-unique-name-generator"}}}}
|
||||
|
||||
driver-namespaces
|
||||
{:linters
|
||||
@@ -839,31 +831,19 @@
|
||||
{:discouraged-namespace
|
||||
{metabase.query-processor.store {:level :off}
|
||||
metabase.util.malli.schema {:message "Prefer metabase.lib.schema or metabase.legacy-mbql.schema schemas in QP/driver code"}
|
||||
toucan.db {:message "Don't use application database directly in QP code; use the MetadataProvider"}
|
||||
toucan.models {:message "Don't use application database directly in QP code; use the MetadataProvider"}
|
||||
toucan.util.test {:message "Don't use application database directly in QP code; use the MetadataProvider"}
|
||||
toucan2.core {:message "Don't use application database directly in QP code; use the MetadataProvider"}}
|
||||
toucan.db {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan.models {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan.util.test {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
toucan2.core {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}
|
||||
|
||||
:discouraged-var
|
||||
{medley.core/map-keys {:message "Use metabase.util.performance/update-keys"}
|
||||
medley.core/map-vals {:message "Use clojure.core/update-vals"}
|
||||
metabase.app-db.core/query {:message "Don't use application database directly in QP code; use the MetadataProvider"}
|
||||
metabase.app-db.core/reducible-query {:message "Don't use application database directly in QP code; use the MetadataProvider"}
|
||||
metabase.app-db.core/query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
metabase.app-db.core/reducible-query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
|
||||
metabase.lib.convert/->legacy-MBQL {:level :off}
|
||||
metabase.lib.core/->legacy-MBQL {:level :off}}}}
|
||||
|
||||
qp-source-namespaces
|
||||
{:linters
|
||||
{:discouraged-namespace
|
||||
{metabase.legacy-mbql.js {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.jvm-util {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.normalize {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.predicates {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.schema {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.schema.helpers {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.schema.macros {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}
|
||||
metabase.legacy-mbql.util {:message "Avoid legacy MBQL namespaces in the QP, use Lib instead"}}}}
|
||||
|
||||
general-qp-and-driver-test-namespaces
|
||||
{:linters
|
||||
{:discouraged-var
|
||||
|
||||
@@ -574,6 +574,7 @@
|
||||
:api #{metabase.legacy-mbql.normalize
|
||||
metabase.legacy-mbql.predicates
|
||||
metabase.legacy-mbql.schema
|
||||
metabase.legacy-mbql.schema.helpers
|
||||
metabase.legacy-mbql.util}
|
||||
:uses #{lib
|
||||
models
|
||||
@@ -590,7 +591,8 @@
|
||||
metabase.lib.field
|
||||
metabase.lib.field.resolution
|
||||
metabase.lib.filter
|
||||
metabase.lib.filter.desugar.jvm ; only so [[metabase.legacy-mbql.jvm-util]] can use defined regexes
|
||||
metabase.lib.filter.desugar
|
||||
metabase.lib.filter.desugar.jvm
|
||||
metabase.lib.init
|
||||
metabase.lib.join.util
|
||||
metabase.lib.metadata
|
||||
@@ -716,7 +718,6 @@
|
||||
metabase.native-query-snippets.core}
|
||||
:uses #{api
|
||||
collections
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -885,6 +886,19 @@
|
||||
enterprise/advanced-permissions
|
||||
enterprise/sandbox}}
|
||||
|
||||
python-runner
|
||||
{:team "Workflows"
|
||||
:api #{metabase.python-runner.api
|
||||
metabase.python-runner.core
|
||||
metabase.python-runner.init}
|
||||
:uses #{api
|
||||
driver
|
||||
models
|
||||
query-processor
|
||||
request
|
||||
util
|
||||
enterprise/transforms}}
|
||||
|
||||
queries
|
||||
{:team "Querying"
|
||||
:api #{metabase.queries.api
|
||||
@@ -1612,17 +1626,6 @@
|
||||
util
|
||||
enterprise/harbormaster}}
|
||||
|
||||
enterprise/comments
|
||||
{:team "Workflows"
|
||||
:api #{metabase-enterprise.comments.api
|
||||
metabase-enterprise.comments.init}
|
||||
:uses #{api
|
||||
events
|
||||
models
|
||||
request
|
||||
users
|
||||
util}}
|
||||
|
||||
enterprise/content-translation
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.content-translation.dictionary
|
||||
@@ -1721,17 +1724,6 @@
|
||||
settings
|
||||
util}}
|
||||
|
||||
enterprise/embedding-hub
|
||||
{:team "Embedding"
|
||||
:api #{metabase-enterprise.embedding-hub.api}
|
||||
:uses #{api
|
||||
appearance
|
||||
audit-app
|
||||
embedding
|
||||
premium-features
|
||||
util
|
||||
enterprise/sso}}
|
||||
|
||||
enterprise/gsheets
|
||||
{:team "Cloud"
|
||||
:api #{metabase-enterprise.gsheets.api
|
||||
@@ -1969,7 +1961,8 @@
|
||||
:api #{metabase-enterprise.transforms.api
|
||||
metabase-enterprise.transforms.core
|
||||
metabase-enterprise.transforms.init
|
||||
metabase-enterprise.transforms.util}
|
||||
;; TODO: Remove this from API once we remove the python-runner callback to metabase
|
||||
metabase-enterprise.transforms.settings}
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
@@ -1987,24 +1980,9 @@
|
||||
sync
|
||||
task
|
||||
task-history
|
||||
;; TODO: Remove this once we're using a better file format than CSV
|
||||
upload
|
||||
util
|
||||
enterprise/transforms-python}}
|
||||
|
||||
enterprise/transforms-python
|
||||
{:team "Querying"
|
||||
:api #{metabase-enterprise.transforms-python.api
|
||||
metabase-enterprise.transforms-python.init
|
||||
metabase-enterprise.transforms-python.settings}
|
||||
:uses #{api
|
||||
app-db
|
||||
config
|
||||
driver
|
||||
models
|
||||
query-processor
|
||||
settings
|
||||
util
|
||||
enterprise/transforms}}
|
||||
util}}
|
||||
|
||||
enterprise/upload-management
|
||||
{:team "UX West"
|
||||
|
||||
@@ -52,13 +52,7 @@
|
||||
|
||||
(defn- lint-require* [node current-ns config]
|
||||
(let [[_require & args] (:children node)
|
||||
required-namespaces (->> (keep unwrap-require args)
|
||||
;; `require` runs through `cc/load-libs` which allows
|
||||
;; #{:as :reload :reload-all :require :use :verbose :refer :as-alias}
|
||||
;; as args. If you use a REPL tool that applies clj-kondo linting (e.g., Clojure MCP)
|
||||
;; you may wish to `:reload` or `:reload-all` a namespace. So we shouldn't assume all
|
||||
;; args are keywords:
|
||||
(remove keyword?))]
|
||||
required-namespaces (keep unwrap-require args)]
|
||||
(lint* node current-ns config required-namespaces)))
|
||||
|
||||
(defn- lint-requiring-resolve* [node current-ns config]
|
||||
|
||||
7
.github/file-paths.yaml
vendored
@@ -41,17 +41,10 @@ frontend_sources: &frontend_sources
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sources:
|
||||
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sdk_sources:
|
||||
- *frontend_configs
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sdk_package_sources:
|
||||
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_specs: &frontend_specs
|
||||
- *shared_specs
|
||||
- "frontend/test/**"
|
||||
|
||||
@@ -1,66 +1,24 @@
|
||||
const TYPES = new Set([
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param title {string}
|
||||
*/
|
||||
function isConventionalTitle(title) {
|
||||
const match = /^(?<type>\w+)\((?<rawScopes>[^)]+)\):/.exec(title);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, type, rawScopes] = match;
|
||||
const scopes = rawScopes
|
||||
.split(",")
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return TYPES.has(type) && scopes.length > 0;
|
||||
}
|
||||
exports.updateComment = updateComment;
|
||||
|
||||
/** @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments */
|
||||
async function updateCommentForEmbedding({ github, context }) {
|
||||
async function updateComment({ github, context }) {
|
||||
const githubUsername = context.payload.sender.login;
|
||||
|
||||
const comment = `${CONVENTIONAL_COMMENT_IDENTIFIER}
|
||||
@${githubUsername} You have modified embedding code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
|
||||
Here are all the supported types:
|
||||
const comment = `${CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER}
|
||||
@${githubUsername} You have modified embedding SDK code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
|
||||
Here are all the supported types that will be shown in the changelog:
|
||||
- \`feat\`
|
||||
- \`fix\`
|
||||
- \`perf\`
|
||||
- \`docs\`
|
||||
- \`style\`
|
||||
- \`refactor\`
|
||||
- \`test\`
|
||||
- \`build\`
|
||||
- \`ci\`
|
||||
|
||||
Please also make sure to include a scope.
|
||||
These types \`docs\`, \`style\`, \`refactor\`, \`test\`, \`build\`, and \`ci\` are also encouraged for non-changelog related tasks.
|
||||
|
||||
Please also make sure to include \`sdk\` scope, otherwise, the changelog will not include this task.
|
||||
|
||||
For example, these are valid PR titles:
|
||||
- \`feat(sdk-bundle): Add interactive dashboard component\`
|
||||
- \`fix(sdk-package): Fixed MetabaseProviderPropsStore logic\`
|
||||
- \`fix(sdk-package, embed-js): Fix API\``;
|
||||
- \`feat(sdk): Add interactive dashboard component\`
|
||||
- \`feat(sdk): Support theming pie chart\`
|
||||
- \`fix(sdk): Fix static dashboard crash\``;
|
||||
|
||||
return updateComment(comment, { github, context });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param comment {string}
|
||||
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
|
||||
*/
|
||||
async function updateComment(comment, { github, context }) {
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pullRequestNumber = context.issue.number;
|
||||
@@ -91,19 +49,20 @@ async function updateComment(comment, { github, context }) {
|
||||
}
|
||||
}
|
||||
|
||||
const CONVENTIONAL_COMMENT_IDENTIFIER =
|
||||
"<!---conventional comment identifier-->";
|
||||
|
||||
const CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER =
|
||||
"<!---conventional commit reminder comment ID-->";
|
||||
/**
|
||||
* @param comments {{body: string}[]}
|
||||
* @typedef Comment
|
||||
* @property {string} body
|
||||
*
|
||||
* @param {Comment[]} comments
|
||||
*/
|
||||
function getExistingConventionalCommitReminderComment(comments) {
|
||||
for (const comment of comments) {
|
||||
if (comment.body.startsWith(CONVENTIONAL_COMMENT_IDENTIFIER)) {
|
||||
if (
|
||||
comment.body.startsWith(CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER)
|
||||
) {
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.isConventionalTitle = isConventionalTitle;
|
||||
exports.updateCommentForEmbedding = updateCommentForEmbedding;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
const { isConventionalTitle } = require("./utils");
|
||||
|
||||
describe("isConventionalTitle", () => {
|
||||
it("returns true for a simple conventional commit", () => {
|
||||
expect(isConventionalTitle("feat: add new feature")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for a commit with a scope", () => {
|
||||
expect(isConventionalTitle("fix(api): correct API bug")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for a commit with multiple scopes, comma separated", () => {
|
||||
expect(isConventionalTitle("feat(build,deps): update dependencies")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns true for a commit with multiple scopes, comma separated with space", () => {
|
||||
expect(isConventionalTitle("feat(build, deps): update dependencies")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for a commit with round brackets in a message", () => {
|
||||
expect(isConventionalTitle("feat: (something) update dependencies")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for missing colon", () => {
|
||||
expect(isConventionalTitle("feat add feature")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for invalid type", () => {
|
||||
expect(isConventionalTitle("invalidType: some message")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty message", () => {
|
||||
expect(isConventionalTitle("feat:")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty input", () => {
|
||||
expect(isConventionalTitle("")).toBe(false);
|
||||
});
|
||||
});
|
||||
84
.github/scripts/get-issue-data.js
vendored
@@ -1,84 +0,0 @@
|
||||
const githubSlackMap = require("../../release/github-slack-map.json");
|
||||
|
||||
async function getIssueData({ context, github, manualIssueNumber }) {
|
||||
let issueNumber;
|
||||
let issueData;
|
||||
let prReviews = [];
|
||||
const triggerType = context.eventName;
|
||||
|
||||
if (context.eventName === "issues") {
|
||||
issueNumber = context.payload.issue.number;
|
||||
issueData = context.payload.issue;
|
||||
} else if (context.eventName === "workflow_dispatch") {
|
||||
issueNumber = parseInt(manualIssueNumber);
|
||||
const { data } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
issueData = data;
|
||||
} else if (context.eventName === "pull_request_review") {
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const { data: prData } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// Extract issue number from branch name (e.g., "claude-fix/issue-12345-some-title")
|
||||
const branchMatch = prData.head.ref.match(/claude-fix\/issue-(\d+)/);
|
||||
if (!branchMatch) {
|
||||
throw new Error(
|
||||
`Could not extract issue number from branch name: ${prData.head.ref}`,
|
||||
);
|
||||
}
|
||||
|
||||
issueNumber = parseInt(branchMatch[1]);
|
||||
|
||||
const { data } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
issueData = data;
|
||||
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
prReviews = reviews.filter(
|
||||
(review) =>
|
||||
review.state === "CHANGES_REQUESTED" &&
|
||||
isMetabaseMember(review.user.login),
|
||||
);
|
||||
}
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
const employeeComments = comments.filter((comment) =>
|
||||
isMetabaseMember(comment.user.login),
|
||||
);
|
||||
|
||||
return {
|
||||
issueNumber,
|
||||
issueData,
|
||||
employeeComments,
|
||||
prReviews,
|
||||
triggerType,
|
||||
isIssueAuthorEmployee: isMetabaseMember(issueData.user.login),
|
||||
};
|
||||
}
|
||||
|
||||
function isMetabaseMember(username) {
|
||||
return username in githubSlackMap;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getIssueData,
|
||||
};
|
||||
42
.github/workflows/backend.yml
vendored
@@ -195,6 +195,47 @@ jobs:
|
||||
- name: Run cljfmt
|
||||
run: ./bin/mage cljfmt-all --force-check
|
||||
|
||||
be-python-runner-tests:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
name: Python Runner Tests
|
||||
env:
|
||||
CI: 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: be-python-runner-tests
|
||||
- name: Start python-runner using Makefile
|
||||
run: |
|
||||
echo "🚀 Starting python-runner using make run..."
|
||||
cd python-runner
|
||||
|
||||
# Use the official make run command
|
||||
make run
|
||||
|
||||
echo "✅ Python runner started successfully!"
|
||||
|
||||
- name: Run Python Runner tests
|
||||
run: >-
|
||||
clojure -X:dev:ee:ee-dev:driver:test:test/python-transform
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: be-python-runner-tests
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
be-tests-result:
|
||||
needs:
|
||||
- be-linter-eastwood
|
||||
@@ -202,6 +243,7 @@ jobs:
|
||||
- be-cljfmt
|
||||
- be-check
|
||||
- be-tests
|
||||
- be-python-runner-tests
|
||||
if: always() && !cancelled()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
280
.github/workflows/claude-code-bug-fix.yml
vendored
@@ -4,22 +4,97 @@ on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue number to fix"
|
||||
required: true
|
||||
type: string
|
||||
override_employee_check:
|
||||
description: "Override employee check for issue creator"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
check-employee-and-issue:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
if: |
|
||||
(github.event_name == 'issues' && github.event.label.name == 'Claude Bug Fix' && github.event.issue.state == 'open') ||
|
||||
(github.event_name == 'workflow_dispatch')
|
||||
outputs:
|
||||
is_employee: ${{ steps.check_employee.outputs.is_employee }}
|
||||
issue_number: ${{ steps.get_issue.outputs.issue_number }}
|
||||
issue_data: ${{ steps.get_issue.outputs.issue_data }}
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Get issue data
|
||||
id: get_issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let issueNumber, issueData;
|
||||
|
||||
if (context.eventName === 'issues') {
|
||||
// Original issue label event
|
||||
issueNumber = context.payload.issue.number;
|
||||
issueData = context.payload.issue;
|
||||
} else if (context.eventName === 'workflow_dispatch') {
|
||||
// Manual trigger with issue number input
|
||||
issueNumber = parseInt('${{ github.event.inputs.issue_number }}');
|
||||
const { data } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
issueData = data;
|
||||
}
|
||||
|
||||
core.setOutput('issue_number', issueNumber);
|
||||
core.setOutput('issue_data', JSON.stringify(issueData));
|
||||
|
||||
console.log(`Issue #${issueNumber}: ${issueData.title}`);
|
||||
console.log(`Issue author: ${issueData.user.login}`);
|
||||
|
||||
- name: Check if issue creator is a Metabase employee
|
||||
id: check_employee
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_DATA: ${{ steps.get_issue.outputs.issue_data }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const issueData = JSON.parse(process.env.ISSUE_DATA);
|
||||
const issueAuthor = issueData.user.login;
|
||||
|
||||
try {
|
||||
// Check if user is a member of the metabase organization (including private members)
|
||||
const { data: membership } = await github.rest.orgs.getMembershipForUser({
|
||||
org: 'metabase',
|
||||
username: issueAuthor
|
||||
});
|
||||
|
||||
if (membership.state === 'active') {
|
||||
console.log(`User ${issueAuthor} is an active member of the Metabase organization. Proceeding with bug fix automation.`);
|
||||
core.setOutput('is_employee', 'true');
|
||||
} else {
|
||||
console.log(`User ${issueAuthor} is not an active member of the Metabase organization. Skipping bug fix automation.`);
|
||||
core.setOutput('is_employee', 'false');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(`User ${issueAuthor} is not a member of the Metabase organization. Skipping bug fix automation.`);
|
||||
} else {
|
||||
console.error('Error checking organization membership:', error);
|
||||
}
|
||||
core.setOutput('is_employee', 'false');
|
||||
}
|
||||
|
||||
auto-fix-bug:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
@@ -28,10 +103,8 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
if: |
|
||||
(github.event_name == 'issues' && github.event.label.name == 'Claude Bug Fix' && github.event.issue.state == 'open') ||
|
||||
(github.event_name == 'workflow_dispatch') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' && startsWith(github.event.pull_request.head.ref, 'claude-fix/'))
|
||||
needs: check-employee-and-issue
|
||||
if: needs.check-employee-and-issue.outputs.is_employee == 'true'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -39,95 +112,67 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get issue data and check employee status
|
||||
id: get_issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { getIssueData } = require('${{ github.workspace }}/.github/scripts/get-issue-data.js');
|
||||
|
||||
try {
|
||||
const result = await getIssueData({
|
||||
context,
|
||||
github,
|
||||
manualIssueNumber: '${{ github.event.inputs.issue_number }}'
|
||||
});
|
||||
|
||||
const { issueNumber, issueData, employeeComments, prReviews, triggerType, isIssueAuthorEmployee } = result;
|
||||
|
||||
// Check employee status and fail if not authorized
|
||||
if (!result.isIssueAuthorEmployee && '${{ github.event.inputs.override_employee_check }}' !== 'true') {
|
||||
core.setFailed(`User ${result.issueData.user.login} is not a Metabase employee. Skipping bug fix automation.`);
|
||||
}
|
||||
|
||||
core.setOutput('issue_number', issueNumber);
|
||||
core.setOutput('issue_data', JSON.stringify(issueData));
|
||||
core.setOutput('issue_comments', JSON.stringify(employeeComments));
|
||||
core.setOutput('pr_reviews', JSON.stringify(prReviews));
|
||||
core.setOutput('trigger_type', triggerType);
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
|
||||
- name: Set up git configuration
|
||||
run: |
|
||||
git config --global user.name "Claude Code[bot]"
|
||||
git config --global user.email "noreply@anthropic.com"
|
||||
|
||||
- name: Prepare issue context and bug fix prompt
|
||||
id: prepare_prompt
|
||||
- name: Prepare dynamic issue info
|
||||
id: prepare_issue_info
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_DATA: ${{ steps.get_issue.outputs.issue_data }}
|
||||
ISSUE_COMMENTS: ${{ steps.get_issue.outputs.issue_comments }}
|
||||
PR_REVIEWS: ${{ steps.get_issue.outputs.pr_reviews }}
|
||||
TRIGGER_TYPE: ${{ steps.get_issue.outputs.trigger_type }}
|
||||
ISSUE_DATA: ${{ needs.check-employee-and-issue.outputs.issue_data }}
|
||||
with:
|
||||
script: |
|
||||
const issueData = JSON.parse(process.env.ISSUE_DATA);
|
||||
const issueComments = JSON.parse(process.env.ISSUE_COMMENTS);
|
||||
const prReviews = JSON.parse(process.env.PR_REVIEWS);
|
||||
const triggerType = process.env.TRIGGER_TYPE;
|
||||
const issueNumber = '${{ steps.get_issue.outputs.issue_number }}';
|
||||
const issueNumber = '${{ needs.check-employee-and-issue.outputs.issue_number }}';
|
||||
|
||||
// Convert issue title to kebab case and create branch name
|
||||
// Convert issue title to kebab case
|
||||
const issueTitle = issueData.title || 'untitled';
|
||||
const kebabTitle = issueTitle
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.substring(0, 50);
|
||||
const branchName = `claude-fix/issue-${issueNumber}-${kebabTitle}`;
|
||||
.substring(0, 50); // Limit length
|
||||
|
||||
// Format data for bash environment
|
||||
const formattedComments = issueComments.length > 0
|
||||
? issueComments.map(comment =>
|
||||
`**Comment by @${comment.user.login} on ${comment.created_at}:**\n${comment.body}`
|
||||
).join('\n\n---\n\n')
|
||||
: '';
|
||||
const formattedReviews = prReviews.length > 0
|
||||
? prReviews.map(review =>
|
||||
`**Review by @${review.user.login} on ${review.submitted_at} (Changes Requested):**\n${review.body || 'No review comment provided'}`
|
||||
).join('\n\n---\n\n')
|
||||
: '';
|
||||
// Create base branch name
|
||||
const baseBranchName = `claude-fix/issue-${issueNumber}-${kebabTitle}`;
|
||||
core.exportVariable('BASE_BRANCH_NAME', baseBranchName);
|
||||
|
||||
core.exportVariable('BRANCH_NAME', branchName);
|
||||
// Export issue info for the prompt
|
||||
core.exportVariable('ISSUE_NUMBER', issueNumber);
|
||||
core.exportVariable('ISSUE_TITLE', issueData.title);
|
||||
core.exportVariable('ISSUE_AUTHOR', issueData.user.login);
|
||||
core.exportVariable('ISSUE_CREATED', issueData.created_at);
|
||||
core.exportVariable('ISSUE_BODY', issueData.body || 'No description provided');
|
||||
core.exportVariable('ISSUE_COMMENTS_FORMATTED', formattedComments);
|
||||
core.exportVariable('PR_REVIEWS_FORMATTED', formattedReviews);
|
||||
core.exportVariable('TRIGGER_TYPE', triggerType);
|
||||
|
||||
- name: Generate bug fix prompt
|
||||
- name: Determine branch strategy
|
||||
run: |
|
||||
# Use the base branch name (no counters)
|
||||
BRANCH_NAME="${{ env.BASE_BRANCH_NAME }}"
|
||||
|
||||
# Check if this exact branch already exists
|
||||
if git branch -r --list "origin/${BRANCH_NAME}" | grep -q .; then
|
||||
echo "Branch already exists: $BRANCH_NAME"
|
||||
echo "Will push to existing branch and skip PR creation"
|
||||
echo "BRANCH_EXISTS=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Creating new branch: $BRANCH_NAME"
|
||||
echo "Will create new PR"
|
||||
echo "BRANCH_EXISTS=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Export the branch name
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Prepare bug fix prompt
|
||||
id: prepare_prompt
|
||||
run: |
|
||||
# Prepare the prompt for Claude
|
||||
cat << 'EOF' >> $GITHUB_ENV
|
||||
BUG_FIX_PROMPT<<PROMPT_END
|
||||
You are helping to fix a bug reported in GitHub issue #${{ env.ISSUE_NUMBER }}.
|
||||
You are helping to automatically fix a bug reported in GitHub issue #${{ env.ISSUE_NUMBER }}.
|
||||
|
||||
## Bug Report Details
|
||||
**Title:** ${{ env.ISSUE_TITLE }}
|
||||
@@ -137,27 +182,8 @@ jobs:
|
||||
**Description:**
|
||||
${{ env.ISSUE_BODY }}
|
||||
|
||||
**Comments:**
|
||||
${{ env.ISSUE_COMMENTS_FORMATTED }}
|
||||
EOF
|
||||
|
||||
# Add review feedback section only if this is a review trigger
|
||||
if [ "${{ env.TRIGGER_TYPE }}" = "pull_request_review" ]; then
|
||||
cat << 'EOF' >> $GITHUB_ENV
|
||||
|
||||
## Previous Fix Attempt - Review Feedback (MUST ADDRESS)
|
||||
Your previous automated fix attempt received the following feedback that needs to be addressed:
|
||||
|
||||
${{ env.PR_REVIEWS_FORMATTED }}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Add the rest of the prompt (always the same)
|
||||
cat << 'EOF' >> $GITHUB_ENV
|
||||
|
||||
## Instructions
|
||||
1. **Analyze the issue description and comments** - Understand what the issue is describing
|
||||
a. Pay particular attention to reproduction steps and if there's an agreed upon fix.
|
||||
1. **Analyze the bug report** - Understand what the issue is describing
|
||||
2. **Search the codebase** - Use Grep and Glob tools to find relevant files
|
||||
3. **Implement a fix** - Follow existing code patterns and conventions
|
||||
4. **Test the fix** - Run appropriate tests using the commands in CLAUDE.md:
|
||||
@@ -179,26 +205,15 @@ jobs:
|
||||
- Only modify actual production code in the main repository directories
|
||||
- The scratchpad will be cleaned up automatically - nothing from it will be committed
|
||||
|
||||
## If You Cannot Fix The Bug
|
||||
If you determine that you cannot create a definitive fix, create a file called `FAILED_FIX_ANALYSIS.md` with:
|
||||
1. **Why No Fix** - Specific reason why you couldn't implement a fix
|
||||
2. **Tool Limitations** - If you needed additional tools/access that weren't available
|
||||
|
||||
Start by understanding the bug and then systematically work toward a solution.
|
||||
PROMPT_END
|
||||
EOF
|
||||
|
||||
- name: Determine branch strategy
|
||||
run: |
|
||||
BRANCH_NAME="${{ env.BRANCH_NAME }}"
|
||||
if git branch -r --list "origin/${BRANCH_NAME}" | grep -q .; then
|
||||
echo "Branch already exists: $BRANCH_NAME"
|
||||
echo "Will push to existing branch and skip PR creation"
|
||||
echo "BRANCH_EXISTS=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Creating new branch: $BRANCH_NAME"
|
||||
echo "Will create new PR"
|
||||
echo "BRANCH_EXISTS=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Export the branch name
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Create scratchpad directory for Claude
|
||||
run: |
|
||||
# Create a scratchpad directory for Claude to use for temporary files
|
||||
@@ -234,6 +249,20 @@ jobs:
|
||||
- name: Check for changes and analysis
|
||||
id: check_changes
|
||||
run: |
|
||||
# Check if Claude created an analysis file first and store its content
|
||||
if [ -f "FAILED_FIX_ANALYSIS.md" ]; then
|
||||
echo "has_analysis=true" >> $GITHUB_OUTPUT
|
||||
echo "Analysis file found"
|
||||
# Store analysis content in a GitHub output (base64 encoded to handle multiline)
|
||||
ANALYSIS_CONTENT=$(base64 -w 0 FAILED_FIX_ANALYSIS.md)
|
||||
echo "analysis_content=$ANALYSIS_CONTENT" >> $GITHUB_OUTPUT
|
||||
# Remove analysis file so it doesn't count as a code change
|
||||
rm -f FAILED_FIX_ANALYSIS.md
|
||||
else
|
||||
echo "has_analysis=false" >> $GITHUB_OUTPUT
|
||||
echo "No analysis file found"
|
||||
fi
|
||||
|
||||
# Now check for actual code changes (excluding analysis file)
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
@@ -249,16 +278,25 @@ jobs:
|
||||
id: prepare_reviewers
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_DATA: ${{ steps.get_issue.outputs.issue_data }}
|
||||
ISSUE_DATA: ${{ needs.check-employee-and-issue.outputs.issue_data }}
|
||||
with:
|
||||
script: |
|
||||
// Get existing team labels from the issue
|
||||
const issueData = JSON.parse(process.env.ISSUE_DATA);
|
||||
const issueLabels = issueData.labels.map(label => label.name);
|
||||
const teamLabels = issueLabels.filter(label => label.startsWith('.Team/'));
|
||||
|
||||
console.log('Issue labels:', issueLabels);
|
||||
console.log('Team labels found:', teamLabels);
|
||||
|
||||
// Add issue creator as reviewer
|
||||
const issueCreator = issueData.user.login;
|
||||
|
||||
core.setOutput('team_labels', teamLabels.join(','));
|
||||
core.setOutput('reviewers', issueCreator);
|
||||
|
||||
console.log('Reviewer:', issueCreator);
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
@@ -325,3 +363,25 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
console.log('🤖 No code changes were made by Claude.');
|
||||
|
||||
// Check if Claude provided a detailed analysis
|
||||
if ('${{ steps.check_changes.outputs.has_analysis }}' === 'true') {
|
||||
try {
|
||||
// Decode the base64-encoded analysis content
|
||||
const analysisBase64 = '${{ steps.check_changes.outputs.analysis_content }}';
|
||||
const analysisContent = Buffer.from(analysisBase64, 'base64').toString('utf8');
|
||||
console.log('📋 Claude provided detailed analysis:');
|
||||
console.log('='.repeat(50));
|
||||
console.log(analysisContent);
|
||||
console.log('='.repeat(50));
|
||||
} catch (error) {
|
||||
console.log('❌ Could not decode analysis content:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ No detailed analysis file was created by Claude.');
|
||||
console.log('This could be because:');
|
||||
console.log('- The issue requires more context or clarification');
|
||||
console.log('- The fix is complex and requires human judgment');
|
||||
console.log('- The issue is not a straightforward code bug');
|
||||
console.log('- Claude couldn\'t locate the relevant code to modify');
|
||||
}
|
||||
|
||||
@@ -4,53 +4,39 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- enterprise/frontend/src/embedding-sdk-bundle/**
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_embedding_sources: ${{ steps.changes.outputs.frontend_embedding_sources }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
pr-title-reminder:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
needs: [ files-changed ]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.files-changed.outputs.frontend_embedding_sources == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Check if the PR title is already following Conventional Commits style
|
||||
uses: actions/github-script@v7
|
||||
id: is-conventional-commit
|
||||
with:
|
||||
result-encoding: string
|
||||
script: | # js
|
||||
const { isConventionalTitle } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
|
||||
const pullRequestTitle = context.payload.pull_request.title
|
||||
const conventionalCommitRegex = /^(?<type>\w+)\((?<scope>\w+)\):/
|
||||
const match = pullRequestTitle.match(conventionalCommitRegex)
|
||||
if (match) {
|
||||
const { type, scope } = match.groups
|
||||
return ['feat', 'fix', 'perf', 'docs', 'style', 'refactor', 'test', 'build', 'ci'].includes(type) && scope === 'sdk'
|
||||
}
|
||||
|
||||
return isConventionalTitle(pullRequestTitle)
|
||||
- name: Checkout repository so `gh` CLI works
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Remind PR authors to use conventional commit in the title
|
||||
if: ${{ steps.is-conventional-commit.outputs.result != 'true' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: | #js
|
||||
const { updateCommentForEmbedding } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
|
||||
|
||||
await updateCommentForEmbedding({github, context})
|
||||
script: |
|
||||
const { updateComment } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
|
||||
await updateComment({github, context})
|
||||
|
||||
@@ -140,10 +140,7 @@ jobs:
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js component \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }} \
|
||||
--env grepTags="-@skip-backward-compatibility",grepOmitFiltered=true
|
||||
run: node e2e/runner/run_cypress_ci.js component --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
|
||||
2
.github/workflows/e2e-test.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
||||
run: |
|
||||
echo "Running tests with specs: $SPEC_PATH"
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--env grepTags="${{inputs.tags && format('{0}+-@skip', inputs.tags) || '-@skip'}}",grepOmitFiltered=true \
|
||||
--env grepTags="${{inputs.tags}}+-@skip",grepOmitFiltered=true \
|
||||
--spec "$SPEC_PATH" \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Embedding SDK Labels Reminder
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [auto_merge_enabled, review_requested, labeled, unlabeled, synchronize]
|
||||
branches:
|
||||
- "master"
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend_embedding_sdk_package_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_package_sources }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
embedding-sdk-package-no-release-notes-reminder:
|
||||
needs: [ files-changed ]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(needs.files-changed.outputs.frontend_embedding_sdk_package_sources == 'true')
|
||||
name: Embedding SDK package release-notes/no-release-notes label reminder
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jesusvasquez333/verify-pr-label-action@v1.4.0
|
||||
id: verify-release-notes-labels
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
valid-labels: 'release-notes, no-release-notes'
|
||||
disable-reviews: true
|
||||
- name: Comment on PR if label should be added
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/actions/notify-pull-request
|
||||
with:
|
||||
remove-previous-comment: true
|
||||
add-new-comment: ${{ steps.verify-release-notes-labels.outcome == 'failure' }}
|
||||
message: "⚠️ Changes were made to the <b>Embedding SDK Package</b>. Review this PR and apply one of the following labels.<br><br>- <code>release-notes</code> — select if this PR must be included in the main Metabase changelog release notes.<br>- <code>no-release-notes</code> — select if this PR should not be included in the main Metabase changelog release notes."
|
||||
include-log: true
|
||||
8
.github/workflows/perf-test-labeled.yml
vendored
@@ -9,13 +9,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
uberjar:
|
||||
if: ${{ github.event.label.name == 'Run-perf' }}
|
||||
uses: ./.github/workflows/uberjar.yml
|
||||
secrets: inherit
|
||||
|
||||
run-perf-test:
|
||||
needs: [uberjar]
|
||||
if: ${{ github.event.label.name == 'Run-perf' }}
|
||||
uses: ./.github/workflows/perf-test.yml
|
||||
with:
|
||||
wait_for_uberjar: true
|
||||
secrets: inherit
|
||||
|
||||
62
.github/workflows/perf-test.yml
vendored
@@ -3,6 +3,11 @@ run-name: Run perf test for ${{ github.ref_name }} by @${{ github.actor }}
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
wait_for_uberjar:
|
||||
description: "Wait for uberjar build"
|
||||
required: false
|
||||
type: boolean
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -26,6 +31,63 @@ jobs:
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
with:
|
||||
registries: "${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}"
|
||||
- name: Wait for uberjar
|
||||
id: wait_for_uberjar
|
||||
if: ${{ inputs.wait_for_uberjar == true }}
|
||||
run: |
|
||||
## Get workflow run id for uberjar build
|
||||
curl -Ls --output e2e-tests.json \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/actions/workflows/run-tests.yml/runs?head_sha=${{ github.event.pull_request.head.sha || github.sha }}
|
||||
ID=$(jq -r '.workflow_runs[0].id' e2e-tests.json)
|
||||
echo "Run ID: ${ID}"
|
||||
|
||||
## Get workflow run id for uberjar build
|
||||
while [ -z "$JOB_ID" ]; do
|
||||
NEXT_URL="https://api.github.com/repos/${{ github.repository }}/actions/runs/${ID}/jobs?filter=latest"
|
||||
|
||||
# Search for build job
|
||||
echo "Searching for build job..."
|
||||
while [ -z "$JOB_ID" ] && [ -n "$NEXT_URL" ]; do
|
||||
# Get one page of jobs
|
||||
RESPONSE=$(curl -s -i \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$NEXT_URL")
|
||||
|
||||
# Extract headers and body
|
||||
HEADERS=$(echo "$RESPONSE" | sed -n '1,/^\r$/p')
|
||||
BODY=$(echo "$RESPONSE" | sed '1,/^\r$/d')
|
||||
LINK_HEADER=$(echo "$HEADERS" | grep -i '^link:' | sed 's/^link: //')
|
||||
|
||||
# Check for next page in Link header
|
||||
if echo "$LINK_HEADER" | grep -q 'rel="next"'; then
|
||||
NEXT_URL=$(echo "$LINK_HEADER" | sed -E 's/.*<([^>]+)>; rel="next".*/\1/')
|
||||
else
|
||||
NEXT_URL=""
|
||||
fi
|
||||
|
||||
# Check for build job
|
||||
JOB_ID=$(echo "$BODY" | jq -r '.jobs[] | select(.name | contains("build (ee)")) | .id')
|
||||
done
|
||||
done
|
||||
echo "Job ID: ${JOB_ID}"
|
||||
|
||||
## Wait for uberjar build to complete
|
||||
while [ true ]; do
|
||||
curl -Ls --output uberjar.json \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/actions/jobs/${JOB_ID}
|
||||
jq -r '.steps[] | select(.name == "Prepare uberjar artifact") | .status' uberjar.json | grep -q "completed" && break
|
||||
echo "Waiting for uberjar build..."
|
||||
sleep 10
|
||||
done
|
||||
echo "run_id=$(jq -r '.workflow_runs[0].id' e2e-tests.json)" >> $GITHUB_OUTPUT
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
|
||||
24
.github/workflows/release-embedding-sdk.yml
vendored
@@ -285,6 +285,10 @@ jobs:
|
||||
run: |
|
||||
./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
|
||||
- name: Update changelog
|
||||
run: |
|
||||
yarn embedding-sdk:generate-changelog -o changelog-diff
|
||||
|
||||
- name: Build SDK package
|
||||
run: yarn run build-embedding-sdk-package
|
||||
|
||||
@@ -297,6 +301,12 @@ jobs:
|
||||
name: metabase-sdk
|
||||
path: ./resources/embedding-sdk
|
||||
|
||||
- name: Upload changelog diff
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sdk-changelog-diff
|
||||
path: ./changelog-diff
|
||||
|
||||
publish-npm:
|
||||
needs: [build-sdk, determine-sdk-version]
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -325,7 +335,19 @@ jobs:
|
||||
run: |
|
||||
bash ./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ env.sdk_version }}
|
||||
|
||||
- name: Create a PR updating readme + published version (using GitHub Metabase Automation account)
|
||||
- name: Retrieve SDK changelog diff
|
||||
if: inputs.branch != 'master'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sdk-changelog-diff
|
||||
|
||||
- name: Update changelog
|
||||
if: inputs.branch != 'master'
|
||||
run: |
|
||||
cat changelog-diff enterprise/frontend/src/embedding-sdk-bundle/CHANGELOG.md > new-changelog
|
||||
mv new-changelog enterprise/frontend/src/embedding-sdk-bundle/CHANGELOG.md
|
||||
|
||||
- name: Create a PR updating readme + published version, and changelog (using GitHub Metabase Automation account)
|
||||
run: |
|
||||
git checkout -b update-sdk-version-${{ env.sdk_version }}
|
||||
|
||||
|
||||
1
.gitignore
vendored
@@ -163,3 +163,4 @@ CLAUDE.local.md
|
||||
|
||||
/logs
|
||||
/.clojure-mcp
|
||||
__pycache__/
|
||||
|
||||
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "python-runner"]
|
||||
path = python-runner
|
||||
url = https://github.com/metabase/python-runner-container.git
|
||||
branch = prototype
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 34 KiB |