diff --git a/eslint.config.js b/eslint.config.js
index 14765ed2c..cdb715cb5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -37,7 +37,10 @@ export default [
"newlines-between": "always",
},
],
- "jsdoc/no-undefined-types": [1, { definedTypes: ["NodeJS"] }],
+ "jsdoc/no-undefined-types": [
+ 1,
+ { definedTypes: ["NodeJS", "RequestInit"] },
+ ],
"jsdoc/require-hyphen-before-param-description": "warn",
"unicorn/comment-content": "off",
"unicorn/consistent-optional-chaining": "off",
diff --git a/helpers/mock-agent/store-bitbucket.js b/helpers/mock-agent/store-bitbucket.js
new file mode 100644
index 000000000..e6e11c7bf
--- /dev/null
+++ b/helpers/mock-agent/store-bitbucket.js
@@ -0,0 +1,110 @@
+import { MockAgent } from "undici";
+
+/**
+ * @param {string} message - Error message
+ * @returns {object} Bitbucket-shaped error response body
+ */
+function errorBody(message) {
+ return { type: "error", error: { message } };
+}
+
+/**
+ * @returns {import("undici").MockAgent} Undici MockAgent
+ * @see {@link https://undici.nodejs.org/#/docs/api/MockAgent}
+ */
+export function mockClient() {
+ const agent = new MockAgent();
+ agent.disableNetConnect();
+
+ const origin = "https://api.bitbucket.org";
+ const sourcePath = "/2.0/repositories/username/repo/src";
+
+ // File exists (foo.txt)
+ agent
+ .get(origin)
+ .intercept({
+ path: `${sourcePath}/main/foo.txt`,
+ method: "GET",
+ query: { format: "meta" },
+ })
+ .reply(200, { path: "foo.txt", type: "commit_file" })
+ .persist();
+
+ // File doesn’t exist (404.txt, new.txt, 401.txt)
+ agent
+ .get(origin)
+ .intercept({
+ path: /\/main\/(404|401|new)\.txt\?format=meta$/,
+ method: "GET",
+ })
+ .reply(404, errorBody("Not found"))
+ .persist();
+
+ // Read file (foo.txt)
+ agent
+ .get(origin)
+ .intercept({ path: `${sourcePath}/main/foo.txt`, method: "GET" })
+ .reply(200, "foo")
+ .persist();
+
+ // Read file (Not found)
+ agent
+ .get(origin)
+ .intercept({ path: `${sourcePath}/main/404.txt`, method: "GET" })
+ .reply(404, errorBody("Not found"))
+ .persist();
+
+ /**
+ * Bitbucket’s `src` endpoint is a single fixed URL for every create,
+ * update and delete — the file path only appears as a field name inside
+ * the multipart request body, which Undici’s `MockAgent` can’t inspect
+ * (it sees a `FormData` body as the literal string `[object FormData]`).
+ * So, rather than matching per file, these are registered as a queue:
+ * each `it()` block below consumes the next one in order.
+ */
+
+ // 1. Creates file (new.txt)
+ agent.get(origin).intercept({ path: sourcePath, method: "POST" }).reply(201, {
+ type: "commit",
+ });
+
+ // 2. Throws error creating file (401.txt)
+ agent
+ .get(origin)
+ .intercept({ path: sourcePath, method: "POST" })
+ .reply(401, errorBody("Unauthorized"));
+
+ // 3. Updates file (foo.txt)
+ agent.get(origin).intercept({ path: sourcePath, method: "POST" }).reply(200, {
+ type: "commit",
+ });
+
+ // 4. Updates and renames file (bar.txt) …
+ agent.get(origin).intercept({ path: sourcePath, method: "POST" }).reply(200, {
+ type: "commit",
+ });
+
+ // …5. … then deletes the old path (foo.txt)
+ agent.get(origin).intercept({ path: sourcePath, method: "POST" }).reply(200, {
+ type: "commit",
+ });
+
+ // 6. Throws error updating file (401.txt)
+ agent
+ .get(origin)
+ .intercept({ path: sourcePath, method: "POST" })
+ .reply(401, errorBody("Unauthorized"));
+
+ // 7. Deletes a file (foo.txt)
+ agent.get(origin).intercept({ path: sourcePath, method: "POST" }).reply(200, {
+ type: "commit",
+ });
+
+ // 8. Throws error deleting a file (401.txt)
+ agent
+ .get(origin)
+ .intercept({ path: sourcePath, method: "POST" })
+ .reply(401, errorBody("Unauthorized"));
+
+ return agent;
+}
diff --git a/package-lock.json b/package-lock.json
index 22e07bf5a..edf9c29cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -61,7 +61,6 @@
"mock-req-res": "^1.2.1",
"mock-session": "^0.0.5",
"mongodb-memory-server": "^11.2.0",
- "nock": "^14.0.16",
"prettier": "^3.9.4",
"stylelint": "^17.14.0",
"stylelint-config-recommended": "^18.0.0",
@@ -5878,24 +5877,6 @@
"sparse-bitfield": "^3.0.3"
}
},
- "node_modules/@mswjs/interceptors": {
- "version": "0.41.9",
- "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz",
- "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@open-draft/deferred-promise": "^2.2.0",
- "@open-draft/logger": "^0.3.0",
- "@open-draft/until": "^2.0.0",
- "is-node-process": "^1.2.0",
- "outvariant": "^1.4.3",
- "strict-event-emitter": "^0.5.1"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz",
@@ -6892,31 +6873,6 @@
"@octokit/openapi-types": "^24.2.0"
}
},
- "node_modules/@open-draft/deferred-promise": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
- "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@open-draft/logger": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
- "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-node-process": "^1.2.0",
- "outvariant": "^1.4.0"
- }
- },
- "node_modules/@open-draft/until": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
- "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@paralleldrive/cuid2": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
@@ -10256,19 +10212,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
- "node_modules/bitbucket": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/bitbucket/-/bitbucket-2.12.0.tgz",
- "integrity": "sha512-YqaiTPEmn5mkwdU2gGcJZcQ6B8/DhCHhc3SSYqSpnef6nSTTSa/2GSBoLEgPLqAuqrQITGKq8MgYkfDMtnJPuw==",
- "license": "MIT",
- "dependencies": {
- "before-after-hook": "^2.1.0",
- "deepmerge": "^4.2.2",
- "is-plain-object": "^3.0.0",
- "node-fetch": "^2.6.0",
- "url-template": "^2.0.8"
- }
- },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -17235,13 +17178,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-node-process": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
- "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -17304,15 +17240,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-plain-object": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
- "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -20206,21 +20133,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/nock": {
- "version": "14.0.16",
- "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.16.tgz",
- "integrity": "sha512-8r4KEc6nT1D/fdLD/R1BO1CPaVEL8o40u/guFRJlXabN7vr3RmMqyjsY5Krt0nMwhsOAwXQ/mtN5vy5Jh3aErg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@mswjs/interceptors": "^0.41.0",
- "json-stringify-safe": "^5.0.1",
- "propagate": "^2.0.0"
- },
- "engines": {
- "node": ">=18.20.0 <20 || >=20.12.1"
- }
- },
"node_modules/node-abi": {
"version": "3.94.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
@@ -20272,54 +20184,12 @@
"semver": "bin/semver.js"
}
},
- "node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"license": "MIT"
},
- "node_modules/node-fetch/node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
- },
- "node_modules/node-fetch/node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/node-fetch/node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "license": "MIT",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/node-forge": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
@@ -21324,13 +21194,6 @@
"node": ">=8"
}
},
- "node_modules/outvariant": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
- "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -22568,16 +22431,6 @@
"node": "^18.17.0 || >=20.5.0"
}
},
- "node_modules/propagate": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
- "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/property-information": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
@@ -24657,13 +24510,6 @@
"text-decoder": "^1.1.0"
}
},
- "node_modules/strict-event-emitter": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
- "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -26518,12 +26364,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/url-template": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
- "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
- "license": "BSD"
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -31556,8 +31396,7 @@
"version": "1.0.0-beta.27",
"license": "MIT",
"dependencies": {
- "@indiekit/error": "^1.0.0-beta.27",
- "bitbucket": "^2.12.0"
+ "@indiekit/error": "^1.0.0-beta.27"
},
"engines": {
"node": ">=24.17"
diff --git a/package.json b/package.json
index f89484b9c..cd7fb37af 100644
--- a/package.json
+++ b/package.json
@@ -97,7 +97,6 @@
"mock-req-res": "^1.2.1",
"mock-session": "^0.0.5",
"mongodb-memory-server": "^11.2.0",
- "nock": "^14.0.16",
"prettier": "^3.9.4",
"stylelint": "^17.14.0",
"stylelint-config-recommended": "^18.0.0",
diff --git a/packages/store-bitbucket/README.md b/packages/store-bitbucket/README.md
index 996d17190..333c13bfe 100644
--- a/packages/store-bitbucket/README.md
+++ b/packages/store-bitbucket/README.md
@@ -8,10 +8,10 @@
## Requirements
-A [Bitbucket app password](https://bitbucket.org/account/settings/app-passwords/).
+A [Bitbucket API token](https://id.atlassian.com/manage-profile/security/api-tokens) with `read:repository:bitbucket` and `write:repository:bitbucket` scopes.
> [!IMPORTANT]
-> Store your app password in an environment variable called `BITBUCKET_PASSWORD` so that only you and the application can see it.
+> Store your app password in an environment variable called `BITBUCKET_TOKEN` so that only you and the application can see it.
## Usage
@@ -21,6 +21,7 @@ Add `@indiekit/store-bitbucket` to your list of plug-ins, specifying options as
{
"plugins": ["@indiekit/store-bitbucket"],
"@indiekit/store-bitbucket": {
+ "email": "username@website.example",
"user": "username",
"repo": "reponame"
}
@@ -29,9 +30,10 @@ Add `@indiekit/store-bitbucket` to your list of plug-ins, specifying options as
## Options
-| Option | Type | Description |
-| :--------- | :------- | :---------------------------------------------------------------------------------- |
-| `user` | `string` | Your Bitbucket username. _Required_. |
-| `repo` | `string` | The name of your Bitbucket repository. _Required_. |
-| `branch` | `string` | The branch files will be saved to. _Optional_, defaults to `main`. |
-| `password` | `string` | A Bitbucket app password. _Required_, defaults to `process.env.BITBUCKET_PASSWORD`. |
+| Option | Type | Description |
+| :------- | :------- | :---------------------------------------------------------------------------- |
+| `email` | `string` | Your Atlassian email. _Required_. |
+| `user` | `string` | Your Bitbucket workspace. _Required_. |
+| `repo` | `string` | The name of your Bitbucket repository. _Required_. |
+| `branch` | `string` | The branch files will be saved to. _Optional_, defaults to `main`. |
+| `token` | `string` | A Bitbucket API token. _Required_, defaults to `process.env.BITBUCKET_TOKEN`. |
diff --git a/packages/store-bitbucket/assets/icon.svg b/packages/store-bitbucket/assets/icon.svg
index 782ac9f3b..417f5eaa1 100644
--- a/packages/store-bitbucket/assets/icon.svg
+++ b/packages/store-bitbucket/assets/icon.svg
@@ -1,4 +1,4 @@
diff --git a/packages/store-bitbucket/index.js b/packages/store-bitbucket/index.js
index 5e87b720a..086a42c76 100644
--- a/packages/store-bitbucket/index.js
+++ b/packages/store-bitbucket/index.js
@@ -1,27 +1,24 @@
+import { Buffer } from "node:buffer";
import path from "node:path";
import process from "node:process";
import { IndiekitError } from "@indiekit/error";
-// eslint-disable-next-line import-x/default
-import bitbucket from "bitbucket";
const defaults = {
branch: "main",
- password: process.env.BITBUCKET_PASSWORD,
+ token: process.env.BITBUCKET_TOKEN,
};
-/**
- * @typedef {import("bitbucket").APIClient} APIClient
- */
export default class BitbucketStore {
name = "Bitbucket store";
/**
* @param {object} [options] - Plug-in options
- * @param {string} [options.user] - Username
+ * @param {string} [options.email] - Atlassian account email address
+ * @param {string} [options.user] - Workspace (used in repository paths)
* @param {string} [options.repo] - Repository
* @param {string} [options.branch] - Branch
- * @param {string} [options.password] - Password
+ * @param {string} [options.token] - API token
*/
constructor(options = {}) {
this.options = { ...defaults, ...options };
@@ -29,22 +26,49 @@ export default class BitbucketStore {
/**
* @access private
- * @returns {APIClient} Bitbucket client interface
+ * @param {string} [requestPath] - Path appended to the repository’s `src` endpoint
+ * @param {RequestInit} [requestOptions] - Fetch options
+ * @returns {Promise} Fetch response
+ * @see {@link https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/}
*/
- get #client() {
- // eslint-disable-next-line import-x/no-named-as-default-member
- const { Bitbucket } = bitbucket;
- return new Bitbucket({
- auth: {
- username: this.options.user,
- password: this.options.password,
- },
- notice: false,
- });
+ async #client(requestPath = "", requestOptions = {}) {
+ const { user, repo, email, token } = this.options;
+ const apiPath = path.join(
+ "2.0/repositories",
+ user,
+ repo,
+ "src",
+ requestPath,
+ );
+ const url = new URL(apiPath, "https://api.bitbucket.org");
+ const credentials = Buffer.from(`${email}:${token}`).toString("base64");
+
+ try {
+ const response = await fetch(url.href, {
+ ...requestOptions,
+ headers: {
+ authorization: `Basic ${credentials}`,
+ ...requestOptions.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const body = await response.json();
+ throw new Error(body.error.message);
+ }
+
+ return response;
+ } catch (error) {
+ throw new IndiekitError(error.message, {
+ cause: error.cause,
+ plugin: this.name,
+ status: error.cause?.status,
+ });
+ }
}
get environment() {
- return ["BITBUCKET_PASSWORD"];
+ return ["BITBUCKET_TOKEN"];
}
get info() {
@@ -61,13 +85,19 @@ export default class BitbucketStore {
{
type: "text",
name: "user",
- message: "What is your Bitbucket username?",
+ message: "What is your Bitbucket workspace?",
},
{
type: "text",
name: "repo",
message: "Which repository is your publication stored on?",
},
+ {
+ type: "text",
+ name: "email",
+ message:
+ "What email address is associated with your Bitbucket account?",
+ },
{
type: "text",
name: "branch",
@@ -81,17 +111,11 @@ export default class BitbucketStore {
* Check if file exists
* @param {string} filePath - Path to file
* @returns {Promise} File exists
- * @see {@link https://bitbucketjs.netlify.app/#api-repositories-repositories_readSrc}
+ * @see {@link https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-commit-path-get}
*/
async fileExists(filePath) {
try {
- await this.#client.repositories.readSrc({
- format: "meta",
- commit: this.options.branch,
- path: filePath,
- repo_slug: this.options.repo,
- workspace: this.options.user,
- });
+ await this.#client(`${this.options.branch}/${filePath}?format=meta`);
return true;
} catch {
@@ -105,62 +129,38 @@ export default class BitbucketStore {
* @param {string} content - File content
* @param {object} options - Options
* @param {string} options.message - Commit message
- * @returns {Promise} Created file URL
- * @see {@link https://bitbucketjs.netlify.app/#api-repositories-repositories_createSrcFileCommit}
+ * @returns {Promise} Created file URL
+ * @see {@link https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-post}
*/
async createFile(filePath, content, { message }) {
- try {
- const fileExists = await this.fileExists(filePath);
- if (fileExists) {
- return;
- }
+ const fileExists = await this.fileExists(filePath);
+ if (fileExists) {
+ return;
+ }
- await this.#client.repositories.createSrcFileCommit({
- [filePath]: content,
- branch: this.options.branch,
- message,
- repo_slug: this.options.repo,
- workspace: this.options.user,
- });
+ const body = new FormData();
+ body.set(filePath, new Blob([content]), path.basename(filePath));
+ body.set("branch", this.options.branch);
+ body.set("message", message);
- const url = new URL(this.info.uid);
- url.pathname = path.join(url.pathname, filePath);
+ await this.#client("", { method: "POST", body });
- return url.href;
- } catch (error) {
- throw new IndiekitError(error.message, {
- cause: error,
- plugin: this.name,
- status: error.status,
- });
- }
+ const url = new URL(this.info.uid);
+ url.pathname = path.join(url.pathname, filePath);
+
+ return url.href;
}
/**
* Read file
* @param {string} filePath - Path to file
* @returns {Promise} File content
- * @see {@link https://bitbucketjs.netlify.app/#api-repositories-repositories_readSrc}
+ * @see {@link https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-commit-path-get}
*/
async readFile(filePath) {
- try {
- const readResponse = await this.#client.repositories.readSrc({
- format: "rendered",
- commit: this.options.branch,
- path: filePath,
- repo_slug: this.options.repo,
- workspace: this.options.user,
- });
- const content = readResponse.data.raw;
+ const response = await this.#client(`${this.options.branch}/${filePath}`);
- return content;
- } catch (error) {
- throw new IndiekitError(error.message, {
- cause: error,
- plugin: this.name,
- status: error.status,
- });
- }
+ return response.text();
}
/**
@@ -171,35 +171,30 @@ export default class BitbucketStore {
* @param {string} options.message - Commit message
* @param {string} [options.newPath] - New path to file
* @returns {Promise} Updated file URL
- * @see {@link https://bitbucketjs.netlify.app/#api-repositories-repositories_createSrcFileCommit}
+ * @see {@link https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-post}
*/
async updateFile(filePath, content, { message, newPath }) {
- try {
- const updateFilePath = newPath || filePath;
-
- await this.#client.repositories.createSrcFileCommit({
- [updateFilePath]: content,
- branch: this.options.branch,
- message,
- repo_slug: this.options.repo,
- workspace: this.options.user,
- });
+ const updateFilePath = newPath || filePath;
- if (newPath) {
- await this.deleteFile(filePath, { message });
- }
+ const body = new FormData();
+ body.set(
+ updateFilePath,
+ new Blob([content]),
+ path.basename(updateFilePath),
+ );
+ body.set("branch", this.options.branch);
+ body.set("message", message);
- const url = new URL(this.info.uid);
- url.pathname = path.join(url.pathname, updateFilePath);
+ await this.#client("", { method: "POST", body });
- return url.href;
- } catch (error) {
- throw new IndiekitError(error.message, {
- cause: error,
- plugin: this.name,
- status: error.status,
- });
+ if (newPath) {
+ await this.deleteFile(filePath, { message });
}
+
+ const url = new URL(this.info.uid);
+ url.pathname = path.join(url.pathname, updateFilePath);
+
+ return url.href;
}
/**
@@ -208,26 +203,17 @@ export default class BitbucketStore {
* @param {object} options - Options
* @param {string} options.message - Commit message
* @returns {Promise} File deleted
- * @see {@link https://bitbucketjs.netlify.app/#api-repositories-repositories_createSrcFileCommit}
+ * @see {@link https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-post}
*/
async deleteFile(filePath, { message }) {
- try {
- await this.#client.repositories.createSrcFileCommit({
- branch: this.options.branch,
- files: filePath,
- message,
- repo_slug: this.options.repo,
- workspace: this.options.user,
- });
+ const body = new FormData();
+ body.set("files", filePath);
+ body.set("branch", this.options.branch);
+ body.set("message", message);
- return true;
- } catch (error) {
- throw new IndiekitError(error.message, {
- cause: error,
- plugin: this.name,
- status: error.status,
- });
- }
+ await this.#client("", { method: "POST", body });
+
+ return true;
}
init(Indiekit) {
diff --git a/packages/store-bitbucket/package.json b/packages/store-bitbucket/package.json
index 956b967e9..5ec508090 100644
--- a/packages/store-bitbucket/package.json
+++ b/packages/store-bitbucket/package.json
@@ -32,8 +32,7 @@
"directory": "packages/store-bitbucket"
},
"dependencies": {
- "@indiekit/error": "^1.0.0-beta.27",
- "bitbucket": "^2.12.0"
+ "@indiekit/error": "^1.0.0-beta.27"
},
"publishConfig": {
"access": "public"
diff --git a/packages/store-bitbucket/test/index.js b/packages/store-bitbucket/test/index.js
index b09123756..0fa5b6d23 100644
--- a/packages/store-bitbucket/test/index.js
+++ b/packages/store-bitbucket/test/index.js
@@ -2,21 +2,22 @@ import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { Indiekit } from "@indiekit/indiekit";
-import nock from "nock";
+import { mockAgent } from "@indiekit-test/mock-agent";
import BitbucketStore from "../index.js";
-describe("store-bitbucket", () => {
+await mockAgent("store-bitbucket");
+
+describe("store-bitbucket", async () => {
const bitbucket = new BitbucketStore({
+ email: "username@website.example",
user: "username",
- password: "password",
+ token: "abcd1234",
repo: "repo",
});
- const bitbucketUrl = "https://api.bitbucket.org";
-
it("Gets plug-in environment", () => {
- assert.deepEqual(bitbucket.environment, ["BITBUCKET_PASSWORD"]);
+ assert.deepEqual(bitbucket.environment, ["BITBUCKET_TOKEN"]);
});
it("Gets plug-in info", () => {
@@ -28,7 +29,7 @@ describe("store-bitbucket", () => {
it("Gets plug-in installation prompts", () => {
assert.equal(
bitbucket.prompts[0].message,
- "What is your Bitbucket username?",
+ "What is your Bitbucket workspace?",
);
});
@@ -50,47 +51,19 @@ describe("store-bitbucket", () => {
});
it("Checks if file exists", async () => {
- nock(bitbucketUrl)
- .get("/2.0/repositories/username/repo/src/main/foo.txt")
- .query({ format: "meta" })
- .reply(201, { path: "foo.txt", type: "meta" });
- nock(bitbucketUrl)
- .post("/2.0/repositories/username/repo/src/main/bar.txt")
- .query({ format: "meta" })
- .replyWithError("Not found");
-
assert.equal(await bitbucket.fileExists("foo.txt"), true);
- assert.equal(await bitbucket.fileExists("bar.txt"), false);
+ assert.equal(await bitbucket.fileExists("404.txt"), false);
});
it("Creates file", async () => {
- nock(bitbucketUrl).post("/2.0/repositories/username/repo/src").reply(201, {
- "content-type": "application/json",
- });
-
- const result = await bitbucket.createFile("foo.txt", "foo", {
+ const result = await bitbucket.createFile("new.txt", "new", {
message: "Message",
});
- assert.equal(result, "https://bitbucket.org/username/repo/foo.txt");
+ assert.equal(result, "https://bitbucket.org/username/repo/new.txt");
});
it("Doesn’t create file if already exists", async () => {
- nock(bitbucketUrl).post("/2.0/repositories/username/repo/src").reply(201, {
- "content-type": "application/json",
- });
-
- // Create file
- await bitbucket.createFile("foo.txt", "foo", {
- message: "Message",
- });
-
- nock(bitbucketUrl)
- .get("/2.0/repositories/username/repo/src/main/foo.txt")
- .query({ format: "meta" })
- .reply(201, { path: "foo.txt", type: "meta" });
-
- // Create file a second time
const result = await bitbucket.createFile("foo.txt", "foo", {
message: "Message",
});
@@ -99,47 +72,27 @@ describe("store-bitbucket", () => {
});
it("Throws error creating file", async () => {
- nock(bitbucketUrl)
- .post("/2.0/repositories/username/repo/src")
- .replyWithError("Not found");
-
await assert.rejects(
- bitbucket.createFile("foo.txt", "foo", { message: "Message" }),
+ bitbucket.createFile("401.txt", "foo", { message: "Message" }),
{
- message:
- "Bitbucket store: request to https://api.bitbucket.org/2.0/repositories/username/repo/src failed, reason: Not found",
+ message: "Bitbucket store: Unauthorized",
},
);
});
it("Reads file", async () => {
- nock(bitbucketUrl)
- .get("/2.0/repositories/username/repo/src/main/foo.txt")
- .query({ format: "rendered" })
- .reply(201, { raw: "foo", type: "rendered" });
-
const result = await bitbucket.readFile("foo.txt");
assert.equal(result, "foo");
});
it("Throws error reading file", async () => {
- nock(bitbucketUrl)
- .get("/2.0/repositories/username/repo/src/main/foo.txt")
- .query({ format: "rendered" })
- .replyWithError("Not found");
-
- await assert.rejects(bitbucket.readFile("foo.txt"), {
- message:
- "Bitbucket store: request to https://api.bitbucket.org/2.0/repositories/username/repo/src/main/foo.txt?format=rendered failed, reason: Not found",
+ await assert.rejects(bitbucket.readFile("404.txt"), {
+ message: "Bitbucket store: Not found",
});
});
it("Updates file", async () => {
- nock(bitbucketUrl).post("/2.0/repositories/username/repo/src").reply(201, {
- "content-type": "application/json",
- });
-
const result = await bitbucket.updateFile("foo.txt", "foo", {
message: "Message",
});
@@ -148,13 +101,6 @@ describe("store-bitbucket", () => {
});
it("Updates and renames file", async () => {
- nock(bitbucketUrl)
- .post("/2.0/repositories/username/repo/src")
- .twice()
- .reply(201, {
- "content-type": "application/json",
- });
-
const result = await bitbucket.updateFile("foo.txt", "foo", {
message: "Message",
newPath: "bar.txt",
@@ -164,24 +110,15 @@ describe("store-bitbucket", () => {
});
it("Throws error updating file", async () => {
- nock(bitbucketUrl)
- .post("/2.0/repositories/username/repo/src")
- .replyWithError("Not found");
-
await assert.rejects(
- bitbucket.updateFile("foo.txt", "foo", { message: "Message" }),
+ bitbucket.updateFile("401.txt", "foo", { message: "Message" }),
{
- message:
- "Bitbucket store: request to https://api.bitbucket.org/2.0/repositories/username/repo/src failed, reason: Not found",
+ message: "Bitbucket store: Unauthorized",
},
);
});
it("Deletes a file", async () => {
- nock(bitbucketUrl).post("/2.0/repositories/username/repo/src").reply(201, {
- "content-type": "application/json",
- });
-
const result = await bitbucket.deleteFile("foo.txt", {
message: "Message",
});
@@ -190,15 +127,10 @@ describe("store-bitbucket", () => {
});
it("Throws error deleting a file", async () => {
- nock(bitbucketUrl)
- .post("/2.0/repositories/username/repo/src")
- .replyWithError("Not found");
-
await assert.rejects(
- bitbucket.deleteFile("foo.txt", { message: "Message" }),
+ bitbucket.deleteFile("401.txt", { message: "Message" }),
{
- message:
- "Bitbucket store: request to https://api.bitbucket.org/2.0/repositories/username/repo/src failed, reason: Not found",
+ message: "Bitbucket store: Unauthorized",
},
);
});