├── .nvmrc ├── test ├── fixtures │ └── files │ │ ├── .dotfile │ │ ├── my-software-v1.0.0.tar.gz │ │ ├── upload.txt │ │ └── upload_other.txt ├── helpers │ └── _mock-gitea.js ├── glob-assets.test.js ├── add-channel.test.js ├── publish.test.js ├── verify.test.js └── integration.test.js ├── lib ├── definitions │ ├── constants.js │ └── errors.js ├── is-prerelease.js ├── get-error.js ├── resolve-config.js ├── parse-git-url.js ├── add-channel.js ├── get-client.js ├── verify.js ├── glob-assets.js └── publish.js ├── .gitignore ├── .npmignore ├── .releaserc.json ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── CONTRIBUTING.md ├── sonar-project.properties ├── CHANGELOG.md ├── index.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13.1 -------------------------------------------------------------------------------- /test/fixtures/files/.dotfile: -------------------------------------------------------------------------------- 1 | Upload file content -------------------------------------------------------------------------------- /test/fixtures/files/my-software-v1.0.0.tar.gz: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/upload.txt: -------------------------------------------------------------------------------- 1 | Upload file content -------------------------------------------------------------------------------- /test/fixtures/files/upload_other.txt: -------------------------------------------------------------------------------- 1 | Upload file content -------------------------------------------------------------------------------- /lib/definitions/constants.js: -------------------------------------------------------------------------------- 1 | const RELEASE_NAME = 'Gitea release'; 2 | 3 | module.exports = {RELEASE_NAME}; 4 | -------------------------------------------------------------------------------- /lib/is-prerelease.js: -------------------------------------------------------------------------------- 1 | module.exports = ({type, main}) => 2 | type === 'prerelease' || (type === 'release' && !main); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | node_modules 4 | dist 5 | !jest.config.js 6 | coverage 7 | docs 8 | .sonar 9 | .scannerwork 10 | *.tgz 11 | pnpm-debug.log 12 | .nyc_output -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/* 2 | test/* 3 | *.ts 4 | tsconfig.json 5 | *.iml 6 | .idea 7 | jest.config.js 8 | .travis.yml 9 | coverage 10 | .nyc_output 11 | .releaserc 12 | sonar-project.properties -------------------------------------------------------------------------------- /lib/get-error.js: -------------------------------------------------------------------------------- 1 | const SemanticReleaseError = require('@semantic-release/error'); 2 | const ERROR_DEFINITIONS = require('./definitions/errors'); 3 | 4 | module.exports = (code, ctx = {}) => { 5 | const {message, details} = ERROR_DEFINITIONS[code](ctx); 6 | return new SemanticReleaseError(message, code, details); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/resolve-config.js: -------------------------------------------------------------------------------- 1 | const {isNil, castArray} = require('lodash'); 2 | 3 | module.exports = ( 4 | { 5 | giteaUrl, 6 | giteaApiPathPrefix, 7 | assets, 8 | }, 9 | {env} 10 | ) => ({ 11 | giteaToken: env.GITEA_TOKEN, 12 | giteaUrl: giteaUrl || env.GITEA_URL, 13 | giteaApiPathPrefix: giteaApiPathPrefix || env.GITEA_PREFIX || '/api/v1', 14 | assets: assets ? castArray(assets) : assets, 15 | }); 16 | -------------------------------------------------------------------------------- /lib/parse-git-url.js: -------------------------------------------------------------------------------- 1 | module.exports = repositoryUrl => { 2 | const [match, auth, host, path] = /^(?!.+:\/\/)(?:(?.*)@)?(?.*?):(?.*)$/.exec(repositoryUrl) || []; 3 | try { 4 | const [, owner, repo] = /^\/(?[^/]+)?\/?(?.+?)(?:\.git)?$/.exec( 5 | new URL(match ? `ssh://${auth ? `${auth}@` : ''}${host}/${path}` : repositoryUrl).pathname 6 | ); 7 | return {owner, repo}; 8 | } catch (_) { 9 | return {}; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saithodev/semantic-release-sharedconf-npm", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/changelog", 7 | [ 8 | "@semantic-release/npm", 9 | {"tarballDir": "release"} 10 | ], 11 | [ 12 | "@semantic-release/git", 13 | {"assets": ["package.json", "CHANGELOG.md"]} 14 | ], 15 | [ 16 | "@semantic-release/github", 17 | {"assets": "release/*.tgz"} 18 | ] 19 | ], 20 | "branches": [ 21 | "master" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Run Tests"] 6 | branches: [master] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - run: npm install 17 | - run: mkdir -p docs 18 | - name: Semantic Release 19 | id: semrel 20 | uses: cycjimmy/semantic-release-action@v2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Feel free to submit any new features via Pull Request. 4 | Make sure the supplied tests still work and to add own test cases to cover your code. 5 | 6 | ## Commit message hook 7 | 8 | This project uses commizen for consistent commit message formatting. 9 | 10 | Please setup a Git hook in `.git/hooks/prepare-commit-msg` with the following contents: 11 | 12 | ```shell script 13 | #!/bin/bash 14 | exec < /dev/tty && node_modules/.bin/git-cz --hook || true 15 | ``` 16 | 17 | After that, make it executable: `chmod +x .git/hooks/prepare-commit-msg` 18 | 19 | ## Running tests 20 | 21 | Run tests by this command: 22 | 23 | ```shell script 24 | npm run test 25 | ``` 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node: [ 10, 11, 12, 14, 15 ] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node }} 17 | - run: npm install 18 | - run: npm test 19 | sonar: 20 | name: SonarQube 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | - name: SonarCloud Scan 27 | uses: sonarsource/sonarcloud-github-action@master 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 31 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=saitho 2 | sonar.projectKey=semantic-release-gitea 3 | 4 | # ===================================================== 5 | # Meta-data for the project 6 | # ===================================================== 7 | 8 | sonar.links.homepage=https://github.com/saitho/semantic-release-gitea 9 | sonar.links.ci=https://travis-ci.com/saitho/semantic-release-gitea 10 | sonar.links.scm=https://github.com/saitho/semantic-release-gitea 11 | sonar.links.issue=https://github.com/saitho/semantic-release-gitea/issues 12 | 13 | 14 | # ===================================================== 15 | # Properties that will be shared amongst all modules 16 | # ===================================================== 17 | 18 | # SQ standard properties 19 | sonar.sources=lib 20 | sonar.tests=test 21 | 22 | # Properties specific to language plugins 23 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.1.0](https://github.com/saitho/semantic-release-gitea/compare/v2.0.1...v2.1.0) (2021-04-08) 2 | 3 | 4 | ### Features 5 | 6 | * process asset path as a template ([e061d07](https://github.com/saitho/semantic-release-gitea/commit/e061d079e6e8d6fbe83a158727a415050cdc288a)) 7 | 8 | ## [2.0.1](https://github.com/saitho/semantic-release-gitea/compare/v2.0.0...v2.0.1) (2021-03-06) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * package.json & package-lock.json to reduce vulnerabilities ([b4332ad](https://github.com/saitho/semantic-release-gitea/commit/b4332ad07a9dfb228364db91c50830b7ff2ec8fa)) 14 | 15 | # [2.0.0](https://github.com/saitho/semantic-release-gitea/compare/v1.0.0...v2.0.0) (2021-01-01) 16 | 17 | 18 | ### Features 19 | 20 | * set fixed Node versions ([f833dcd](https://github.com/saitho/semantic-release-gitea/commit/f833dcdf9929b5248803c410751b4d01da1ba39f)) 21 | 22 | 23 | ### BREAKING CHANGES 24 | 25 | * The package can only be installed with the following node versions: 10, 11, 12, >=14 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint require-atomic-updates: off */ 2 | 3 | const {defaultTo, castArray} = require('lodash'); 4 | const verifyGitea = require('./lib/verify'); 5 | const addChannelGitea = require('./lib/add-channel'); 6 | const publishGitea = require('./lib/publish'); 7 | 8 | let verified; 9 | 10 | async function verifyConditions(pluginConfig, context) { 11 | const {options} = context; 12 | // If the Gitea publish plugin is used and has `assets`, `labels` or `assignees` configured, validate it now in order to prevent any release if the configuration is wrong 13 | if (options.publish) { 14 | const publishPlugin = 15 | castArray(options.publish).find( 16 | (config) => config.path && config.path === '@saithodev/semantic-release-gitea' 17 | ) || {}; 18 | 19 | pluginConfig.assets = defaultTo( 20 | pluginConfig.assets, 21 | publishPlugin.assets 22 | ); 23 | } 24 | 25 | await verifyGitea(pluginConfig, context); 26 | verified = true; 27 | } 28 | 29 | async function publish(pluginConfig, context) { 30 | if (!verified) { 31 | await verifyGitea(pluginConfig, context); 32 | verified = true; 33 | } 34 | 35 | return publishGitea(pluginConfig, context); 36 | } 37 | 38 | async function addChannel(pluginConfig, context) { 39 | if (!verified) { 40 | await verifyGitea(pluginConfig, context); 41 | verified = true; 42 | } 43 | 44 | return addChannelGitea(pluginConfig, context); 45 | } 46 | 47 | module.exports = {verifyConditions, addChannel, publish}; 48 | -------------------------------------------------------------------------------- /lib/add-channel.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('saithodev:semantic-release-gitea'); 2 | const {RELEASE_NAME} = require('./definitions/constants'); 3 | const parseGitUrl = require('./parse-git-url'); 4 | const resolveConfig = require('./resolve-config'); 5 | const getClient = require('./get-client'); 6 | const isPrerelease = require('./is-prerelease'); 7 | 8 | module.exports = async (pluginConfig, context) => { 9 | const { 10 | options: {repositoryUrl}, 11 | branch, 12 | nextRelease: {name, gitTag, notes}, 13 | logger, 14 | } = context; 15 | const {giteaToken, giteaUrl, giteaApiPathPrefix} = resolveConfig(pluginConfig, context); 16 | const {owner, repo} = parseGitUrl(repositoryUrl); 17 | const gitea = getClient(giteaToken, giteaUrl, giteaApiPathPrefix); 18 | let releaseId; 19 | 20 | const release = {prerelease: isPrerelease(branch), tag_name: gitTag, name: name}; 21 | debug('release object: %O', release); 22 | 23 | let hasTag = true; 24 | try { 25 | const releaseByTag = await gitea.getReleaseByTag(owner, repo, gitTag); 26 | releaseId = releaseByTag.id; 27 | } catch (error) { 28 | if (error.hasOwnProperty('response') && error.response.statusCode === 404) { 29 | hasTag = false; 30 | } else { 31 | throw error; 32 | } 33 | } 34 | 35 | let url = ''; 36 | 37 | if (!hasTag) { 38 | logger.log('There is no release for tag %s, creating a new one', gitTag); 39 | 40 | release.body = notes; 41 | const response = await gitea.createRelease(owner, repo, release); 42 | const parsedResponse = JSON.parse(response.body); 43 | url = parsedResponse.url; 44 | 45 | logger.log('Published Gitea release: %s', url); 46 | } else { 47 | debug('release release_id: %o', releaseId); 48 | 49 | const responseUpdate = await gitea.updateRelease(owner, repo, releaseId, release); 50 | const parsedResponseUpdate = JSON.parse(responseUpdate.body); 51 | url = parsedResponseUpdate.url; 52 | 53 | logger.log('Updated Gitea release: %s', url); 54 | } 55 | 56 | return {url, name: RELEASE_NAME}; 57 | }; 58 | -------------------------------------------------------------------------------- /test/helpers/_mock-gitea.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a `nock` object setup to respond to a Gitea authentication request. Other expectation and responses can be chained. 3 | * 4 | * @param {Object} [env={}] Environment variables. 5 | * @param {String} [giteaToken=env.GITEA_TOKEN || 'GITEA_TOKEN'] The github token to return in the authentication response. 6 | * @param {String} [giteaUrl=env.GITEA_URL || 'https://api.gitea.io'] The url on which to intercept http requests. 7 | * @param {String} [giteaApiPathPrefix=env.GITEA_PREFIX || ''] The GitHub Enterprise API prefix. 8 | * @return {Object} A `nock` object ready to respond to a github authentication request. 9 | */ 10 | export function authenticate( 11 | env = {}, 12 | { 13 | giteaToken = env.GITEA_TOKEN, 14 | giteaUrl = env.GITEA_URL, 15 | giteaApiPathPrefix = env.GITEA_PREFIX || '/api/v1', 16 | } = {} 17 | ) { 18 | const urlJoin = require('url-join'); 19 | const nock = require('nock'); 20 | return nock( 21 | urlJoin(giteaUrl, giteaApiPathPrefix), 22 | {reqheaders: {'Authorization': 'token ' + giteaToken, 'Content-Type': 'application/json'}} 23 | ); 24 | } 25 | 26 | /** 27 | * Return a `nock` object setup to respond to a Gitea upload request. Other expectation and responses can be chained. 28 | * 29 | * @param {Object} [env={}] Environment variables. 30 | * @param {String} [giteaToken=env.GITEA_TOKEN || 'GITEA_TOKEN'] The github token to return in the authentication response. 31 | * @param {String} [giteaUrl=env.GITEA_URL || 'https://api.gitea.io'] The url on which to intercept http requests. 32 | * @param {String} [giteaApiPathPrefix=env.GITEA_PREFIX || ''] The GitHub Enterprise API prefix. 33 | * @return {Object} A `nock` object ready to respond to a github authentication request. 34 | */ 35 | export function upload( 36 | env = {}, 37 | { 38 | giteaToken = env.GITEA_TOKEN, 39 | giteaUrl = env.GITEA_URL, 40 | giteaApiPathPrefix = env.GITEA_PREFIX || '/api/v1', 41 | } = {} 42 | ) { 43 | const urlJoin = require('url-join'); 44 | const nock = require('nock'); 45 | return nock( 46 | urlJoin(giteaUrl, giteaApiPathPrefix), 47 | {reqheaders: {'Authorization': 'token ' + giteaToken}} 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saithodev/semantic-release-gitea", 3 | "version": "2.1.0", 4 | "description": "Publish release notes to Gitea", 5 | "main": "index.js", 6 | "scripts": { 7 | "codecov": "codecov -f coverage/coverage-final.json", 8 | "lint": "xo", 9 | "semantic-release": "semantic-release", 10 | "test": "nyc ava -v", 11 | "commit": "git-cz" 12 | }, 13 | "engines": { 14 | "node": "^10 || ^11 || ^12 || >=14" 15 | }, 16 | "dependencies": { 17 | "@semantic-release/error": "^2.2.0", 18 | "aggregate-error": "^3.0.0", 19 | "debug": "^4.0.0", 20 | "dir-glob": "^3.0.0", 21 | "form-data": "^3.0.0", 22 | "fs-extra": "^8.0.0", 23 | "globby": "^10.0.0", 24 | "got": "^10.0.1", 25 | "lodash": "^4.17.21", 26 | "querystring": "^0.2.0", 27 | "url-join": "^4.0.0" 28 | }, 29 | "devDependencies": { 30 | "@saithodev/semantic-release-sharedconf-npm": "^2.0.2", 31 | "ava": "^2.0.0", 32 | "clear-module": "^4.0.0", 33 | "codecov": "^3.5.0", 34 | "commitizen": "^4.0.3", 35 | "cz-conventional-changelog": "^3.0.2", 36 | "nock": "^11.1.0", 37 | "nyc": "^15.0.0", 38 | "proxyquire": "^2.0.0", 39 | "semantic-release": "^17.4.2", 40 | "sinon": "^8.0.0", 41 | "tempy": "^0.3.0", 42 | "xo": "^0.38.2" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/saitho/semantic-release-gitea" 47 | }, 48 | "author": "Mario Lubenka", 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/saitho/semantic-release-gitea/issues" 52 | }, 53 | "homepage": "https://github.com/saitho/semantic-release-gitea#readme", 54 | "keywords": [ 55 | "git", 56 | "gitea", 57 | "publish", 58 | "pull-request", 59 | "release", 60 | "semantic-release", 61 | "version" 62 | ], 63 | "config": { 64 | "commitizen": { 65 | "path": "./node_modules/cz-conventional-changelog" 66 | } 67 | }, 68 | "nyc": { 69 | "include": [ 70 | "lib/**/*.js", 71 | "index.js" 72 | ], 73 | "reporter": [ 74 | "lcov", 75 | "text", 76 | "html" 77 | ], 78 | "all": true 79 | }, 80 | "xo": { 81 | "prettier": true, 82 | "space": true, 83 | "rules": { 84 | "camelcase": [ 85 | "error", 86 | { 87 | "properties": "never" 88 | } 89 | ] 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/get-client.js: -------------------------------------------------------------------------------- 1 | const FormData = require('form-data'); 2 | const {createReadStream} = require('fs'); 3 | const got = require('got'); 4 | const urlJoin = require('url-join'); 5 | const querystring = require('querystring'); 6 | 7 | class GiteaClient { 8 | constructor(giteaToken, giteaUrl, giteaApiPathPrefix) { 9 | this.baseUrl = urlJoin(giteaUrl, giteaApiPathPrefix); 10 | this.token = giteaToken; 11 | } 12 | 13 | _makeRequest(verb, apiPath, body) { 14 | const fullUrl = urlJoin(this.baseUrl, apiPath); 15 | const apiOptions = {headers: {'Authorization': 'token ' + this.token, 'Content-Type': 'application/json'}}; 16 | 17 | if (body instanceof FormData) { 18 | delete apiOptions.headers["Content-Type"]; 19 | } else if (typeof body === 'object') { 20 | body = JSON.stringify(body); 21 | } 22 | 23 | switch (verb) { 24 | case 'post': 25 | return got.post(fullUrl, {...apiOptions, body: body}); 26 | case 'patch': 27 | return got.patch(fullUrl, {...apiOptions, body: body}); 28 | default: 29 | return got.get(fullUrl, apiOptions); 30 | } 31 | } 32 | 33 | createReleaseAsset(owner, repo, releaseId, assetName, filePath) { 34 | const form = new FormData(); 35 | form.append('attachment', createReadStream(filePath)); 36 | 37 | return this._makeRequest( 38 | 'post', 39 | `/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${querystring.escape(assetName)}`, 40 | form 41 | ); 42 | } 43 | 44 | updateRelease(owner, repo, releaseId, data) { 45 | return this._makeRequest('patch', `/repos/${owner}/${repo}/releases/${releaseId}`, data); 46 | } 47 | 48 | createRelease(owner, repo, release) { 49 | return this._makeRequest('post', `/repos/${owner}/${repo}/releases`, release); 50 | } 51 | 52 | async getReleaseByTag(owner, repo, gitTag) { 53 | let page = 1; 54 | while(true) { 55 | const request = await this._makeRequest('get', `/repos/${owner}/${repo}/releases?page=${page}`); 56 | const releases = JSON.parse(request.body); 57 | if(!releases.length) { 58 | break; 59 | } 60 | for (const release of releases) { 61 | if (release.tag_name === gitTag) { 62 | return release; 63 | } 64 | } 65 | } 66 | throw {response: { statusCode: 404} }; 67 | } 68 | 69 | getRepo(repo, owner) { 70 | return this._makeRequest('get', `/repos/${owner}/${repo}`); 71 | } 72 | } 73 | 74 | module.exports = (giteaToken, giteaUrl, giteaApiPathPrefix) => { 75 | return new GiteaClient(giteaToken, giteaUrl, giteaApiPathPrefix); 76 | }; 77 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | const {isString, isPlainObject, isNil, isArray, isNumber} = require('lodash'); 2 | const urlJoin = require('url-join'); 3 | const AggregateError = require('aggregate-error'); 4 | const parseGitUrl = require('./parse-git-url'); 5 | const resolveConfig = require('./resolve-config'); 6 | const getClient = require('./get-client'); 7 | const getError = require('./get-error'); 8 | 9 | const isNonEmptyString = value => isString(value) && value.trim(); 10 | const isStringOrStringArray = value => isNonEmptyString(value) || (isArray(value) && value.every(isNonEmptyString)); 11 | const isArrayOf = validator => array => isArray(array) && array.every(value => validator(value)); 12 | 13 | const VALIDATORS = { 14 | /** 15 | * @use getError.EINVALIDASSETS 16 | */ 17 | assets: isArrayOf( 18 | asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)) 19 | ), 20 | }; 21 | 22 | module.exports = async (pluginConfig, context) => { 23 | const { 24 | options: {repositoryUrl}, 25 | logger, 26 | } = context; 27 | const {giteaToken, giteaUrl, giteaApiPathPrefix, ...options} = resolveConfig(pluginConfig, context); 28 | 29 | const errors = Object.entries({...options}).reduce( 30 | (errs, [option, value]) => 31 | !isNil(value) && !VALIDATORS[option](value) 32 | ? [...errs, getError(`EINVALID${option.toUpperCase()}`, {[option]: value})] 33 | : errs, 34 | [] 35 | ); 36 | 37 | 38 | const {repo, owner} = parseGitUrl(repositoryUrl); 39 | 40 | if (!giteaUrl) { 41 | errors.push(getError('ENOGITEAURL')); 42 | } 43 | 44 | if (!giteaToken) { 45 | errors.push(getError('ENOGITEATOKEN', {owner, repo})); 46 | } 47 | 48 | if (!owner || !repo) { 49 | errors.push(getError('EINVALIDGITEAURL')); 50 | } else if(giteaUrl && giteaToken) { 51 | logger.log('Verify Gitea authentication (' + urlJoin(giteaUrl, giteaApiPathPrefix) + ')'); 52 | try { 53 | const gitea = getClient(giteaToken, giteaUrl, giteaApiPathPrefix); 54 | const response = await gitea.getRepo(repo, owner); 55 | const parsedResponse = JSON.parse(response.body); 56 | if (!parsedResponse.permissions.push) { 57 | errors.push(getError('EGITEANOPERMISSION', {owner, repo})); 58 | } 59 | } catch (error) { 60 | if (error.hasOwnProperty('response')) { 61 | switch (error.response.statusCode) { 62 | case 401: 63 | errors.push(getError('EINVALIDGITEATOKEN', {owner, repo})); 64 | break; 65 | case 404: 66 | errors.push(getError('EMISSINGREPO', {owner, repo})); 67 | break; 68 | } 69 | } else { 70 | throw error; 71 | } 72 | } 73 | } 74 | 75 | if (errors.length > 0) { 76 | throw new AggregateError(errors); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /lib/glob-assets.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {basename} = require('path'); 3 | const {isPlainObject, castArray, uniqWith, uniq} = require('lodash'); 4 | const dirGlob = require('dir-glob'); 5 | const globby = require('globby'); 6 | const debug = require('debug')('saithodev:semantic-release-gitea'); 7 | 8 | module.exports = async ({cwd}, assets) => 9 | uniqWith( 10 | [] 11 | .concat( 12 | ...(await Promise.all( 13 | assets.map(async asset => { 14 | // Wrap single glob definition in Array 15 | let glob = castArray(isPlainObject(asset) ? asset.path : asset); 16 | // TODO Temporary workaround for https://github.com/mrmlnc/fast-glob/issues/47 17 | glob = uniq([...(await dirGlob(glob, {cwd})), ...glob]); 18 | 19 | // Skip solo negated pattern (avoid to include every non js file with `!**/*.js`) 20 | if (glob.length <= 1 && glob[0].startsWith('!')) { 21 | debug( 22 | 'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files', 23 | glob[0] 24 | ); 25 | return []; 26 | } 27 | 28 | const globbed = await globby(glob, { 29 | cwd, 30 | expandDirectories: false, // TODO Temporary workaround for https://github.com/mrmlnc/fast-glob/issues/47 31 | gitignore: false, 32 | dot: true, 33 | onlyFiles: false, 34 | }); 35 | 36 | if (isPlainObject(asset)) { 37 | if (globbed.length > 1) { 38 | // If asset is an Object with a glob the `path` property that resolve to multiple files, 39 | // Output an Object definition for each file matched and set each one with: 40 | // - `path` of the matched file 41 | // - `name` based on the actual file name (to avoid assets with duplicate `name`) 42 | // - other properties of the original asset definition 43 | return globbed.map(file => ({...asset, path: file, name: basename(file)})); 44 | } 45 | 46 | // If asset is an Object, output an Object definition with: 47 | // - `path` of the matched file if there is one, or the original `path` definition (will be considered as a missing file) 48 | // - other properties of the original asset definition 49 | return {...asset, path: globbed[0] || asset.path}; 50 | } 51 | 52 | if (globbed.length > 0) { 53 | // If asset is a String definition, output each files matched 54 | return globbed; 55 | } 56 | 57 | // If asset is a String definition but no match is found, output the elements of the original glob (each one will be considered as a missing file) 58 | return glob; 59 | }) 60 | // Sort with Object first, to prioritize Object definition over Strings in dedup 61 | )) 62 | ) 63 | .sort(asset => (isPlainObject(asset) ? -1 : 1)), 64 | // Compare `path` property if Object definition, value itself if String 65 | (a, b) => path.resolve(cwd, isPlainObject(a) ? a.path : a) === path.resolve(cwd, isPlainObject(b) ? b.path : b) 66 | ); 67 | -------------------------------------------------------------------------------- /lib/definitions/errors.js: -------------------------------------------------------------------------------- 1 | const {inspect} = require('util'); 2 | const {isString} = require('lodash'); 3 | const pkg = require('../../package.json'); 4 | 5 | const [homepage] = pkg.homepage.split('#'); 6 | const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5})); 7 | const linkify = file => `${homepage}/blob/master/${file}`; 8 | 9 | module.exports = { 10 | EINVALIDASSETS: ({assets}) => ({ 11 | message: 'Invalid `assets` option.', 12 | details: `The [assets option](${linkify( 13 | 'README.md#assets' 14 | )}) must be an \`Array\` of \`Strings\` or \`Objects\` with a \`path\` property. 15 | 16 | Your configuration for the \`assets\` option is \`${stringify(assets)}\`.`, 17 | }), 18 | ENOGITEAURL: () => ({ 19 | message: 'No Gitea URL was provided.', 20 | details: `The [Gitea URL](${linkify( 21 | 'README.md#gitea-authentication' 22 | )}) configured in the \`GITEA_URL\` environment variable must be a valid Gitea url.`, 23 | }), 24 | EINVALIDGITEAURL: () => ({ 25 | message: 'The git repository URL is not a valid Gitea URL.', 26 | details: `The **semantic-release** \`repositoryUrl\` option must a valid Git URL with the format \`//.git\`. 27 | 28 | By default the \`repositoryUrl\` option is retrieved from the \`repository\` property of your \`package.json\` or the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) of the repository cloned by your CI environment.`, 29 | }), 30 | EMISSINGREPO: ({owner, repo}) => ({ 31 | message: `The repository ${owner}/${repo} doesn't exist.`, 32 | details: `The **semantic-release** \`repositoryUrl\` option must refer to your Gitea repository. The repository must be accessible with the Gitea API. 33 | 34 | By default the \`repositoryUrl\` option is retrieved from the \`repository\` property of your \`package.json\` or the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) of the repository cloned by your CI environment. 35 | 36 | Please make sure to configure the \`giteaUrl\` and \`giteaApiPathPrefix\` [options](${linkify( 37 | 'README.md#options' 38 | )}).`, 39 | }), 40 | EGITEANOPERMISSION: ({owner, repo}) => ({ 41 | message: `The Gitea token doesn't allow to push on the repository ${owner}/${repo}.`, 42 | details: `The user associated with the [Gitea token](${linkify( 43 | 'README.md#gitea-authentication' 44 | )}) configured in the \`GITEA_TOKEN\` environment variable must allows to push to the repository ${owner}/${repo}. 45 | 46 | Please make sure the Gitea user associated with the token is an owner or a collaborator if the repository belong to a user account or has write permissions if the repository belongs to an organization.`, 47 | }), 48 | EINVALIDGITEATOKEN: ({owner, repo}) => ({ 49 | message: 'Invalid Gitea token.', 50 | details: `The [Gitea token](${linkify( 51 | 'README.md#gitea-authentication' 52 | )}) configured in the \`GITEA_TOKEN\` environment variable must be a valid personal token allowing to push to the repository ${owner}/${repo}. 53 | 54 | Please make sure to set the \`GITEA_TOKEN\` environment variable in your CI with the exact value of the Gitea personal token.`, 55 | }), 56 | ENOGITEATOKEN: ({owner, repo}) => ({ 57 | message: 'No Gitea token specified.', 58 | details: `A [Gitea personal token](${linkify( 59 | 'README.md#gitea-authentication' 60 | )}) must be created and set in the \`GITEA_TOKEN\` environment variable on your CI environment. 61 | 62 | Please make sure to create a Gitea personal token and to set it in the \`GITEA_TOKEN\` environment variable on your CI environment. The token must allow to push to the repository ${owner}/${repo}.`, 63 | }), 64 | EGITEAAPIERROR: ({message}) => ({ 65 | message: 'Gitea API reported an error.', 66 | details: `Gitea API reported the following error: ${message}.`, 67 | }), 68 | }; 69 | -------------------------------------------------------------------------------- /lib/publish.js: -------------------------------------------------------------------------------- 1 | const {basename, resolve} = require('path'); 2 | const {stat} = require('fs-extra'); 3 | const {isPlainObject, template} = require('lodash'); 4 | const debug = require('debug')('saithodev:semantic-release-gitea'); 5 | const {RELEASE_NAME} = require('./definitions/constants'); 6 | const parseGitUrl = require('./parse-git-url'); 7 | const globAssets = require('./glob-assets.js'); 8 | const resolveConfig = require('./resolve-config'); 9 | const getClient = require('./get-client'); 10 | const isPrerelease = require('./is-prerelease'); 11 | const getError = require('./get-error'); 12 | 13 | module.exports = async (pluginConfig, context) => { 14 | const { 15 | cwd, 16 | options: {repositoryUrl}, 17 | branch, 18 | nextRelease: {name, gitTag, notes}, 19 | logger, 20 | } = context; 21 | const {giteaToken, giteaUrl, giteaApiPathPrefix, assets} = resolveConfig(pluginConfig, context); 22 | const {owner, repo} = parseGitUrl(repositoryUrl); 23 | const gitea = getClient(giteaToken, giteaUrl, giteaApiPathPrefix); 24 | const release = {tag_name: gitTag, name, body: notes, prerelease: isPrerelease(branch)}; 25 | 26 | debug('release object: %O', release); 27 | 28 | // When there are no assets, we publish a release directly 29 | if (!assets || assets.length === 0) { 30 | const responseDirectRelease = await gitea.createRelease(owner, repo, release); 31 | const parsedResponseDirectRelease = JSON.parse(responseDirectRelease.body); 32 | const releaseUrl = parsedResponseDirectRelease.url; 33 | 34 | logger.log('Published Gitea release: %s', releaseUrl); 35 | return {url: releaseUrl, name: RELEASE_NAME}; 36 | } 37 | 38 | // We'll create a draft release, append the assets to it, and then publish it. 39 | // This is so that the assets are available when we get a Gitea release event. 40 | const draftRelease = {...release, draft: true}; 41 | 42 | const response = await gitea.createRelease(owner, repo, draftRelease); 43 | const parsedResponse = JSON.parse(response.body); 44 | const releaseId = parsedResponse.id; 45 | 46 | // Append assets to the release 47 | const globbedAssets = await globAssets(context, assets); 48 | debug('globed assets: %o', globbedAssets); 49 | 50 | await Promise.all( 51 | globbedAssets.map(async asset => { 52 | const filePath = template(isPlainObject(asset) ? asset.path : asset)(context); 53 | const fullFilePath = resolve(cwd, filePath); 54 | let file; 55 | 56 | try { 57 | file = await stat(fullFilePath); 58 | } catch (_) { 59 | logger.error('The asset %s cannot be read, and will be ignored.', filePath); 60 | return; 61 | } 62 | 63 | if (!file || !file.isFile()) { 64 | logger.error('The asset %s is not a file, and will be ignored.', filePath); 65 | return; 66 | } 67 | 68 | let assetName = template(asset.name || basename(filePath))(context); 69 | 70 | debug('file path: %o', filePath); 71 | debug('asset name: %o', assetName); 72 | 73 | if (isPlainObject(asset) && asset.label) { 74 | assetName = template(asset.label)(context); 75 | } 76 | 77 | try { 78 | const responseAsset = await gitea.createReleaseAsset(owner, repo, releaseId, assetName, fullFilePath); 79 | const parsedResponseAsset = JSON.parse(responseAsset.body); 80 | const downloadUrl = parsedResponseAsset.browser_download_url; 81 | logger.log('Published file %s', downloadUrl); 82 | } catch (e) { 83 | logger.log('API error while publishing file %s', filePath); 84 | if (e.hasOwnProperty('response') && e.response.body.length) { 85 | const errorBody = JSON.parse(e.response.body); 86 | throw getError('EGITEAAPIERROR', {message: errorBody.message}); 87 | } 88 | throw e; 89 | } 90 | }) 91 | ); 92 | 93 | const responseUpdate = await gitea.updateRelease(owner, repo, releaseId, {draft: false}); 94 | const parsedResponseUpdate = JSON.parse(responseUpdate.body); 95 | const url = parsedResponseUpdate.url; 96 | 97 | logger.log('Published Gitea release: %s', url); 98 | return {url: url, name: RELEASE_NAME}; 99 | }; 100 | -------------------------------------------------------------------------------- /test/glob-assets.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import test from 'ava'; 3 | import {copy, ensureDir} from 'fs-extra'; 4 | import {isPlainObject, sortBy} from 'lodash'; 5 | import tempy from 'tempy'; 6 | import globAssets from '../lib/glob-assets'; 7 | 8 | const sortAssets = assets => sortBy(assets, asset => (isPlainObject(asset) ? asset.path : asset)); 9 | 10 | const fixtures = 'test/fixtures/files'; 11 | 12 | test('Retrieve file from single path', async t => { 13 | const cwd = tempy.directory(); 14 | await copy(fixtures, cwd); 15 | const globbedAssets = await globAssets({cwd}, ['upload.txt']); 16 | 17 | t.deepEqual(globbedAssets, ['upload.txt']); 18 | }); 19 | 20 | test('Retrieve multiple files from path', async t => { 21 | const cwd = tempy.directory(); 22 | await copy(fixtures, cwd); 23 | const globbedAssets = await globAssets({cwd}, ['upload.txt', 'upload_other.txt']); 24 | 25 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['upload_other.txt', 'upload.txt'])); 26 | }); 27 | 28 | test('Include missing files as defined, using Object definition', async t => { 29 | const cwd = tempy.directory(); 30 | await copy(fixtures, cwd); 31 | const globbedAssets = await globAssets({cwd}, ['upload.txt', {path: 'miss*.txt', label: 'Missing'}]); 32 | 33 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['upload.txt', {path: 'miss*.txt', label: 'Missing'}])); 34 | }); 35 | 36 | test('Retrieve multiple files from Object', async t => { 37 | const cwd = tempy.directory(); 38 | await copy(fixtures, cwd); 39 | const globbedAssets = await globAssets({cwd}, [ 40 | {path: 'upload.txt', name: 'upload_name', label: 'Upload label'}, 41 | 'upload_other.txt', 42 | ]); 43 | 44 | t.deepEqual( 45 | sortAssets(globbedAssets), 46 | sortAssets([{path: 'upload.txt', name: 'upload_name', label: 'Upload label'}, 'upload_other.txt']) 47 | ); 48 | }); 49 | 50 | test('Retrieve multiple files without duplicates', async t => { 51 | const cwd = tempy.directory(); 52 | await copy(fixtures, cwd); 53 | const globbedAssets = await globAssets({cwd}, [ 54 | 'upload_other.txt', 55 | 'upload.txt', 56 | 'upload_other.txt', 57 | 'upload.txt', 58 | 'upload.txt', 59 | 'upload_other.txt', 60 | ]); 61 | 62 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['upload_other.txt', 'upload.txt'])); 63 | }); 64 | 65 | test('Favor Object over String values when removing duplicates', async t => { 66 | const cwd = tempy.directory(); 67 | await copy(fixtures, cwd); 68 | const globbedAssets = await globAssets({cwd}, [ 69 | 'upload_other.txt', 70 | 'upload.txt', 71 | {path: 'upload.txt', name: 'upload_name'}, 72 | 'upload.txt', 73 | {path: 'upload_other.txt', name: 'upload_other_name'}, 74 | 'upload.txt', 75 | 'upload_other.txt', 76 | ]); 77 | 78 | t.deepEqual( 79 | sortAssets(globbedAssets), 80 | sortAssets([ 81 | {path: 'upload.txt', name: 'upload_name'}, 82 | {path: 'upload_other.txt', name: 'upload_other_name'}, 83 | ]) 84 | ); 85 | }); 86 | 87 | test('Retrieve file from single glob', async t => { 88 | const cwd = tempy.directory(); 89 | await copy(fixtures, cwd); 90 | const globbedAssets = await globAssets({cwd}, ['upload.*']); 91 | 92 | t.deepEqual(globbedAssets, ['upload.txt']); 93 | }); 94 | 95 | test('Retrieve multiple files from single glob', async t => { 96 | const cwd = tempy.directory(); 97 | await copy(fixtures, cwd); 98 | const globbedAssets = await globAssets({cwd}, ['*.txt']); 99 | 100 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['upload_other.txt', 'upload.txt'])); 101 | }); 102 | 103 | test('Accept glob array with one value', async t => { 104 | const cwd = tempy.directory(); 105 | await copy(fixtures, cwd); 106 | const globbedAssets = await globAssets({cwd}, [['*load.txt'], ['*_other.txt']]); 107 | 108 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['upload_other.txt', 'upload.txt'])); 109 | }); 110 | 111 | test('Include globs that resolve to no files as defined', async t => { 112 | const cwd = tempy.directory(); 113 | await copy(fixtures, cwd); 114 | const globbedAssets = await globAssets({cwd}, [['upload.txt', '!upload.txt']]); 115 | 116 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['!upload.txt', 'upload.txt'])); 117 | }); 118 | 119 | test('Accept glob array with one value for missing files', async t => { 120 | const cwd = tempy.directory(); 121 | await copy(fixtures, cwd); 122 | const globbedAssets = await globAssets({cwd}, [['*missing.txt'], ['*_other.txt']]); 123 | 124 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['upload_other.txt', '*missing.txt'])); 125 | }); 126 | 127 | test('Replace name by filename for Object that match multiple files', async t => { 128 | const cwd = tempy.directory(); 129 | await copy(fixtures, cwd); 130 | const globbedAssets = await globAssets({cwd}, [{path: '*.txt', name: 'upload_name', label: 'Upload label'}]); 131 | 132 | t.deepEqual( 133 | sortAssets(globbedAssets), 134 | sortAssets([ 135 | {path: 'upload.txt', name: 'upload.txt', label: 'Upload label'}, 136 | {path: 'upload_other.txt', name: 'upload_other.txt', label: 'Upload label'}, 137 | ]) 138 | ); 139 | }); 140 | 141 | test('Include dotfiles', async t => { 142 | const cwd = tempy.directory(); 143 | await copy(fixtures, cwd); 144 | const globbedAssets = await globAssets({cwd}, ['.dot*']); 145 | 146 | t.deepEqual(globbedAssets, ['.dotfile']); 147 | }); 148 | 149 | test('Ingnore single negated glob', async t => { 150 | const cwd = tempy.directory(); 151 | await copy(fixtures, cwd); 152 | const globbedAssets = await globAssets({cwd}, ['!*.txt']); 153 | 154 | t.deepEqual(globbedAssets, []); 155 | }); 156 | 157 | test('Ingnore single negated glob in Object', async t => { 158 | const cwd = tempy.directory(); 159 | await copy(fixtures, cwd); 160 | const globbedAssets = await globAssets({cwd}, [{path: '!*.txt'}]); 161 | 162 | t.deepEqual(globbedAssets, []); 163 | }); 164 | 165 | test('Accept negated globs', async t => { 166 | const cwd = tempy.directory(); 167 | await copy(fixtures, cwd); 168 | const globbedAssets = await globAssets({cwd}, [['*.txt', '!**/*_other.txt']]); 169 | 170 | t.deepEqual(globbedAssets, ['upload.txt']); 171 | }); 172 | 173 | test('Expand directories', async t => { 174 | const cwd = tempy.directory(); 175 | await copy(fixtures, path.resolve(cwd, 'dir')); 176 | const globbedAssets = await globAssets({cwd}, [['dir']]); 177 | 178 | t.deepEqual(sortAssets(globbedAssets), sortAssets(['dir', 'dir/my-software-v1.0.0.tar.gz', 'dir/upload_other.txt', 'dir/upload.txt', 'dir/.dotfile'])); 179 | }); 180 | 181 | test('Include empty directory as defined', async t => { 182 | const cwd = tempy.directory(); 183 | await copy(fixtures, cwd); 184 | await ensureDir(path.resolve(cwd, 'empty')); 185 | const globbedAssets = await globAssets({cwd}, [['empty']]); 186 | 187 | t.deepEqual(globbedAssets, ['empty']); 188 | }); 189 | 190 | test('Deduplicate resulting files path', async t => { 191 | const cwd = tempy.directory(); 192 | await copy(fixtures, cwd); 193 | const globbedAssets = await globAssets({cwd}, ['./upload.txt', path.resolve(cwd, 'upload.txt'), 'upload.txt']); 194 | 195 | t.is(globbedAssets.length, 1); 196 | }); 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @saithodev/semantic-release-gitea 2 | 3 | [**semantic-release**](https://github.com/semantic-release/semantic-release) plugin to publish a Gitea release. 4 | 5 | [![Build Status](https://travis-ci.com/saitho/semantic-release-gitea.svg?branch=master)](https://travis-ci.com/saitho/semantic-release-gitea) 6 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 7 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 8 | 9 | | Step | Description | 10 | |--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 11 | | `verifyConditions` | Verify the presence and the validity of the authentication (set via [environment variables](#environment-variables)) and the [assets](#assets) option configuration. | 12 | | `publish` | Publish a Gitea release, optionally uploading file assets. | 13 | | `addChannel` | Update a Gitea release's `pre-release` field. | 14 | 15 | This plugin is based on the [semantic-release GitHub plugin](https://github.com/semantic-release/github). Thanks to everyone who worked on that! 16 | 17 | ## Install 18 | 19 | ```bash 20 | $ npm install @saithodev/semantic-release-gitea -D 21 | ``` 22 | 23 | ## Usage 24 | 25 | The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration): 26 | 27 | ```json 28 | { 29 | "plugins": [ 30 | "@semantic-release/commit-analyzer", 31 | "@semantic-release/release-notes-generator", 32 | ["@saithodev/semantic-release-gitea", { 33 | "giteaUrl": "https://try.gitea.io", 34 | "assets": [ 35 | {"path": "dist/asset.min.css", "label": "CSS distribution"}, 36 | {"path": "dist/asset.min.js", "label": "JS distribution"} 37 | ] 38 | }], 39 | ] 40 | } 41 | ``` 42 | 43 | With this example Gitea releases will be published with the file `dist/asset.min.css` and `dist/asset.min.js`. 44 | 45 | ## Configuration 46 | 47 | ### Gitea authentication 48 | 49 | The Gitea authentication configuration is **required** and can be set via [environment variables](#environment-variables). 50 | 51 | Create a API key token via your Gitea installation’s web interface: `Settings | Applications | Generate New Token.`. 52 | The token has to be made available in your CI environment via the `GITEA_TOKEN` environment variable. 53 | The user associated with the token must have push permission to the repository. 54 | 55 | ### Environment variables 56 | 57 | | Variable | Description | 58 | | ------------------------------ | ----------------------------------------- | 59 | | `GITEA_TOKEN` | **Required.** The token used to authenticate with Gitea. | 60 | | `GITEA_URL` | **Required.** The URL to your Gitea instance. | 61 | | `GITEA_PREFIX` | The Gitea API prefix. (default: /api/v1) | 62 | 63 | ### Options 64 | 65 | | Option | Description | Default | 66 | |----------------------|--------------------------------------------------------------------|--------------------------------------| 67 | | `giteaUrl` | The Gitea endpoint. | `GITEA_URL` environment variable. | 68 | | `giteaApiPathPrefix` | The Gitea API prefix. | `GITEA_PREFIX` environment variable. | 69 | | `assets` | An array of files to upload to the release. See [assets](#assets). | - | 70 | 71 | #### assets 72 | 73 | Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array` of 74 | [globs](https://github.com/isaacs/node-glob#glob-primer) and `Object`s with the following properties: 75 | 76 | | Property | Description | Default | 77 | | -------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ | 78 | | `path` | **Required.** A [glob](https://github.com/isaacs/node-glob#glob-primer) to identify the files to upload. | - | 79 | | `name` | The name of the downloadable file on the GitHub release. | File name extracted from the `path`. | 80 | | `label` | Short description of the file displayed on the GitHub release. | - | 81 | 82 | Each entry in the `assets` `Array` is globbed individually. A [glob](https://github.com/isaacs/node-glob#glob-primer) 83 | can be a `String` (`"dist/**/*.js"` or `"dist/mylib.js"`) or an `Array` of `String`s that will be globbed together 84 | (`["dist/**", "!**/*.css"]`). 85 | 86 | If a directory is configured, all the files under this directory and its children will be included. 87 | 88 | The `name` and `label` for each assets are generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: 89 | 90 | | Parameter | Description | 91 | |---------------|-------------------------------------------------------------------------------------| 92 | | `branch` | The branch from which the release is done. | 93 | | `lastRelease` | `Object` with `version`, `gitTag` and `gitHead` of the last release. | 94 | | `nextRelease` | `Object` with `version`, `gitTag`, `gitHead` and `notes` of the release being done. | 95 | | `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | 96 | 97 | **Note**: If a file has a match in `assets` it will be included even if it also has a match in `.gitignore`. 98 | 99 | **Note**: The file types in this example are per default not allowed for release assets. 100 | Make sure to check your Gitea configuration for allowed file types (setting `AttachmentAllowedTypes` inside `[attachment]` scope). 101 | 102 | ##### assets examples 103 | 104 | `'dist/*.js'`: include all the `js` files in the `dist` directory, but not in its sub-directories. 105 | 106 | `[['dist', '!**/*.css']]`: include all the files in the `dist` directory and its sub-directories excluding the `css` 107 | files. 108 | 109 | `[{path: 'dist/MyLibrary.js', label: 'MyLibrary JS distribution'}, {path: 'dist/MyLibrary.css', label: 'MyLibrary CSS 110 | distribution'}]`: include the `dist/MyLibrary.js` and `dist/MyLibrary.css` files, and label them `MyLibrary JS 111 | distribution` and `MyLibrary CSS distribution` in the Gitea release. 112 | 113 | `[['dist/**/*.{js,css}', '!**/*.min.*'], {path: 'build/MyLibrary.zip', label: 'MyLibrary'}]`: include all the `js` and 114 | `css` files in the `dist` directory and its sub-directories excluding the minified version, plus the 115 | `build/MyLibrary.zip` file and label it `MyLibrary` in the Gitea release. 116 | 117 | `[{path: 'dist/MyLibrary.js', name: 'MyLibrary-${nextRelease.gitTag}.js', label: 'MyLibrary JS (${nextRelease.gitTag}) distribution'}]`: include the file `dist/MyLibrary.js` and upload it to the Gitea release with name `MyLibrary-v1.0.0.js` and label `MyLibrary JS (v1.0.0) distribution` which will generate the link: 118 | 119 | > `[MyLibrary JS (v1.0.0) distribution](MyLibrary-v1.0.0.js)` 120 | -------------------------------------------------------------------------------- /test/add-channel.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import nock from 'nock'; 3 | import {stub} from 'sinon'; 4 | import {authenticate} from './helpers/_mock-gitea'; 5 | 6 | /* eslint camelcase: ["error", {properties: "never"}] */ 7 | 8 | // const cwd = 'test/fixtures/files'; 9 | const addChannel = require('../lib/add-channel'); 10 | 11 | test.beforeEach(t => { 12 | // Mock logger 13 | t.context.log = stub(); 14 | t.context.error = stub(); 15 | t.context.logger = {log: t.context.log, error: t.context.error}; 16 | }); 17 | 18 | test.afterEach.always(() => { 19 | // Clear nock 20 | nock.cleanAll(); 21 | }); 22 | 23 | test.serial('Update a release', async t => { 24 | const owner = 'test_user'; 25 | const repo = 'test_repo'; 26 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 27 | const pluginConfig = {}; 28 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 29 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 30 | const releaseId = 1; 31 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 32 | 33 | const gitea = authenticate(env) 34 | .get(`/repos/${owner}/${repo}/releases?page=1`) 35 | .reply(200, [{id: releaseId, tag_name: nextRelease.gitTag}]) 36 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 37 | tag_name: nextRelease.gitTag, 38 | name: nextRelease.name, 39 | prerelease: false, 40 | }) 41 | .reply(200, {url: releaseUrl}); 42 | 43 | const result = await addChannel(pluginConfig, { 44 | env, 45 | options, 46 | branch: {type: 'release', main: true}, 47 | nextRelease, 48 | logger: t.context.logger, 49 | }); 50 | 51 | t.is(result.url, releaseUrl); 52 | t.deepEqual(t.context.log.args[0], ['Updated Gitea release: %s', releaseUrl]); 53 | t.true(gitea.isDone()); 54 | }); 55 | 56 | test.serial('Update a maintenance release', async t => { 57 | const owner = 'test_user'; 58 | const repo = 'test_repo'; 59 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 60 | const pluginConfig = {}; 61 | const nextRelease = {gitTag: 'v1.0.0', channel: '1.x', name: 'v1.0.0', notes: 'Test release note body'}; 62 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 63 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 64 | const releaseId = 1; 65 | 66 | const gitea = authenticate(env) 67 | .get(`/repos/${owner}/${repo}/releases?page=1`) 68 | .reply(200, [{id: releaseId, "tag_name": nextRelease.gitTag}]) 69 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 70 | tag_name: nextRelease.gitTag, 71 | name: nextRelease.name, 72 | prerelease: false, 73 | }) 74 | .reply(200, {url: releaseUrl}); 75 | 76 | const result = await addChannel(pluginConfig, { 77 | env, 78 | options, 79 | branch: {type: 'maintenance', channel: '1.x', main: false}, 80 | nextRelease, 81 | logger: t.context.logger, 82 | }); 83 | 84 | t.is(result.url, releaseUrl); 85 | t.deepEqual(t.context.log.args[0], ['Updated Gitea release: %s', releaseUrl]); 86 | t.true(gitea.isDone()); 87 | }); 88 | 89 | test.serial('Update a prerelease', async t => { 90 | const owner = 'test_user'; 91 | const repo = 'test_repo'; 92 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 93 | const pluginConfig = {}; 94 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 95 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 96 | const releaseId = 1; 97 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 98 | 99 | const gitea = authenticate(env) 100 | .get(`/repos/${owner}/${repo}/releases?page=1`) 101 | .reply(200, [{id: releaseId, "tag_name": nextRelease.gitTag}]) 102 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 103 | tag_name: nextRelease.gitTag, 104 | name: nextRelease.name, 105 | prerelease: false, 106 | }) 107 | .reply(200, {url: releaseUrl}); 108 | 109 | const result = await addChannel(pluginConfig, { 110 | env, 111 | options, 112 | branch: {type: 'maintenance', channel: '1.x', main: false}, 113 | nextRelease, 114 | logger: t.context.logger, 115 | }); 116 | 117 | t.is(result.url, releaseUrl); 118 | t.deepEqual(t.context.log.args[0], ['Updated Gitea release: %s', releaseUrl]); 119 | t.true(gitea.isDone()); 120 | }); 121 | 122 | test.serial('Create the new release if current one is missing', async t => { 123 | const owner = 'test_user'; 124 | const repo = 'test_repo'; 125 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 126 | const pluginConfig = {}; 127 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 128 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 129 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 130 | 131 | const gitea = authenticate(env) 132 | .get(`/repos/${owner}/${repo}/releases?page=1`) 133 | .reply(404) 134 | .post(`/repos/${owner}/${repo}/releases`, { 135 | tag_name: nextRelease.gitTag, 136 | name: nextRelease.name, 137 | body: nextRelease.notes, 138 | prerelease: false, 139 | }) 140 | .reply(200, {url: releaseUrl}); 141 | 142 | const result = await addChannel(pluginConfig, { 143 | env, 144 | options, 145 | branch: {type: 'release', main: true}, 146 | nextRelease, 147 | logger: t.context.logger, 148 | }); 149 | 150 | t.is(result.url, releaseUrl); 151 | t.deepEqual(t.context.log.args[0], ['There is no release for tag %s, creating a new one', nextRelease.gitTag]); 152 | t.deepEqual(t.context.log.args[1], ['Published Gitea release: %s', releaseUrl]); 153 | t.true(gitea.isDone()); 154 | }); 155 | 156 | test.serial('Throw error if cannot read current release', async t => { 157 | const owner = 'test_user'; 158 | const repo = 'test_repo'; 159 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 160 | const pluginConfig = {}; 161 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 162 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 163 | 164 | const gitea = authenticate(env) 165 | .get(`/repos/${owner}/${repo}/releases?page=1`) 166 | .reply(500); 167 | 168 | await t.throwsAsync( 169 | addChannel(pluginConfig, { 170 | env, 171 | options, 172 | branch: {type: 'release', main: true}, 173 | nextRelease, 174 | logger: t.context.logger, 175 | }) 176 | ); 177 | 178 | t.true(gitea.isDone()); 179 | }); 180 | 181 | test.serial('Throw error if cannot create missing current release', async t => { 182 | const owner = 'test_user'; 183 | const repo = 'test_repo'; 184 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 185 | const pluginConfig = {}; 186 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 187 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 188 | 189 | const gitea = authenticate(env) 190 | .get(`/repos/${owner}/${repo}/releases?page=1`) 191 | .reply(404) 192 | .post(`/repos/${owner}/${repo}/releases`, { 193 | tag_name: nextRelease.gitTag, 194 | name: nextRelease.name, 195 | body: nextRelease.notes, 196 | prerelease: false, 197 | }) 198 | .reply(500); 199 | 200 | await t.throwsAsync( 201 | addChannel(pluginConfig, { 202 | env, 203 | options, 204 | branch: {type: 'release', main: true}, 205 | nextRelease, 206 | logger: t.context.logger, 207 | }) 208 | ); 209 | 210 | t.true(gitea.isDone()); 211 | }); 212 | 213 | test.serial('Throw error if cannot update release', async t => { 214 | const owner = 'test_user'; 215 | const repo = 'test_repo'; 216 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 217 | const pluginConfig = {}; 218 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 219 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 220 | const releaseId = 1; 221 | 222 | const github = authenticate(env) 223 | .get(`/repos/${owner}/${repo}/releases?page=1`) 224 | .reply(200, [{id: releaseId, "tag_name": nextRelease.gitTag}]) 225 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 226 | tag_name: nextRelease.gitTag, 227 | name: nextRelease.name, 228 | prerelease: false, 229 | }) 230 | .reply(404); 231 | 232 | await t.throwsAsync( 233 | addChannel(pluginConfig, { 234 | env, 235 | options, 236 | branch: {type: 'release', main: true}, 237 | nextRelease, 238 | logger: t.context.logger, 239 | }) 240 | ); 241 | t.true(github.isDone()); 242 | }); 243 | -------------------------------------------------------------------------------- /test/publish.test.js: -------------------------------------------------------------------------------- 1 | import {escape} from 'querystring'; 2 | import test from 'ava'; 3 | import nock from 'nock'; 4 | import {stub} from 'sinon'; 5 | import tempy from 'tempy'; 6 | import {authenticate, upload} from './helpers/_mock-gitea'; 7 | 8 | /* eslint camelcase: ["error", {properties: "never"}] */ 9 | 10 | const cwd = 'test/fixtures/files'; 11 | const publish = require('../lib/publish'); 12 | 13 | test.beforeEach(t => { 14 | // Mock logger 15 | t.context.log = stub(); 16 | t.context.error = stub(); 17 | t.context.logger = {log: t.context.log, error: t.context.error}; 18 | }); 19 | 20 | test.afterEach.always(() => { 21 | // Clear nock 22 | nock.cleanAll(); 23 | }); 24 | 25 | test.serial('Publish a release', async t => { 26 | const owner = 'test_user'; 27 | const repo = 'test_repo'; 28 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 29 | const pluginConfig = {}; 30 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 31 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 32 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/1`; 33 | 34 | const gitea = authenticate(env) 35 | .post(`/repos/${owner}/${repo}/releases`, { 36 | tag_name: nextRelease.gitTag, 37 | name: nextRelease.name, 38 | body: nextRelease.notes, 39 | prerelease: false, 40 | }) 41 | .reply(200, {url: releaseUrl}); 42 | 43 | const result = await publish(pluginConfig, { 44 | cwd, 45 | env, 46 | options, 47 | branch: {type: 'release', main: true}, 48 | nextRelease, 49 | logger: t.context.logger, 50 | }); 51 | 52 | t.is(result.url, releaseUrl); 53 | t.deepEqual(t.context.log.args[0], ['Published Gitea release: %s', releaseUrl]); 54 | t.true(gitea.isDone()); 55 | }); 56 | 57 | test.serial('Publish a release on a channel', async t => { 58 | const owner = 'test_user'; 59 | const repo = 'test_repo'; 60 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 61 | const pluginConfig = {}; 62 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 63 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 64 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 65 | 66 | const gitea = authenticate(env) 67 | .post(`/repos/${owner}/${repo}/releases`, { 68 | tag_name: nextRelease.gitTag, 69 | name: nextRelease.name, 70 | body: nextRelease.notes, 71 | prerelease: true, 72 | }) 73 | .reply(200, {url: releaseUrl}); 74 | 75 | const result = await publish(pluginConfig, { 76 | cwd, 77 | env, 78 | options, 79 | branch: {type: 'release', channel: 'next', main: false}, 80 | nextRelease, 81 | logger: t.context.logger, 82 | }); 83 | 84 | t.is(result.url, releaseUrl); 85 | t.deepEqual(t.context.log.args[0], ['Published Gitea release: %s', releaseUrl]); 86 | t.true(gitea.isDone()); 87 | }); 88 | 89 | test.serial('Publish a prerelease', async t => { 90 | const owner = 'test_user'; 91 | const repo = 'test_repo'; 92 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 93 | const pluginConfig = {}; 94 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 95 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 96 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 97 | 98 | const gitea = authenticate(env) 99 | .post(`/repos/${owner}/${repo}/releases`, { 100 | tag_name: nextRelease.gitTag, 101 | name: nextRelease.name, 102 | body: nextRelease.notes, 103 | prerelease: true, 104 | }) 105 | .reply(200, {url: releaseUrl}); 106 | 107 | const result = await publish(pluginConfig, { 108 | cwd, 109 | env, 110 | options, 111 | branch: {type: 'prerelease', channel: 'beta'}, 112 | nextRelease, 113 | logger: t.context.logger, 114 | }); 115 | 116 | t.is(result.url, releaseUrl); 117 | t.deepEqual(t.context.log.args[0], ['Published Gitea release: %s', releaseUrl]); 118 | t.true(gitea.isDone()); 119 | }); 120 | 121 | test.serial('Publish a maintenance release', async t => { 122 | const owner = 'test_user'; 123 | const repo = 'test_repo'; 124 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 125 | const pluginConfig = {}; 126 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 127 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 128 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 129 | const releaseId = 1; 130 | 131 | const gitea = authenticate(env) 132 | .post(`/repos/${owner}/${repo}/releases`, { 133 | tag_name: nextRelease.gitTag, 134 | name: nextRelease.name, 135 | body: nextRelease.notes, 136 | prerelease: false, 137 | }) 138 | .reply(200, {url: releaseUrl, id: releaseId}); 139 | 140 | const result = await publish(pluginConfig, { 141 | cwd, 142 | env, 143 | options, 144 | branch: {type: 'maintenance', channel: '1.x', main: false}, 145 | nextRelease, 146 | logger: t.context.logger, 147 | }); 148 | 149 | t.is(result.url, releaseUrl); 150 | t.deepEqual(t.context.log.args[0], ['Published Gitea release: %s', releaseUrl]); 151 | t.true(gitea.isDone()); 152 | }); 153 | 154 | test.serial('Publish a release with one asset', async t => { 155 | const owner = 'test_user'; 156 | const repo = 'test_repo'; 157 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 158 | const pluginConfig = { 159 | assets: [{path: '.dotfile', label: 'A dotfile with no ext'}], 160 | }; 161 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 162 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 163 | const untaggedReleaseUrl = `https://gitea.io/${owner}/${repo}/releases/untagged-123`; 164 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 165 | const assetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${nextRelease.version}/.dotfile`; 166 | const releaseId = 1; 167 | 168 | const gitea = authenticate(env) 169 | .post(`/repos/${owner}/${repo}/releases`, { 170 | tag_name: nextRelease.gitTag, 171 | name: nextRelease.name, 172 | body: nextRelease.notes, 173 | draft: true, 174 | prerelease: false, 175 | }) 176 | .reply(200, { url: untaggedReleaseUrl, id: releaseId}) 177 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, {draft: false}) 178 | .reply(200, {url: releaseUrl}); 179 | 180 | const giteaUpload = upload(env) 181 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('A dotfile with no ext')}`) 182 | .reply(200, {browser_download_url: assetUrl}); 183 | 184 | const result = await publish(pluginConfig, { 185 | cwd, 186 | env, 187 | options, 188 | branch: {type: 'release', main: true}, 189 | nextRelease, 190 | logger: t.context.logger, 191 | }); 192 | 193 | t.is(result.url, releaseUrl); 194 | t.true(t.context.log.calledWith('Published Gitea release: %s', releaseUrl)); 195 | t.true(t.context.log.calledWith('Published file %s', assetUrl)); 196 | t.true(gitea.isDone()); 197 | t.true(giteaUpload.isDone()); 198 | }); 199 | 200 | test.serial('Publish a release with a asset containing the next release version', async t => { 201 | const owner = 'test_user'; 202 | const repo = 'test_repo'; 203 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 204 | const pluginConfig = { 205 | assets: [{path: 'my-software-v${nextRelease.version}.tar.gz', label: 'Version ${nextRelease.version} of my software'}], 206 | }; 207 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body', version: '1.0.0'}; 208 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 209 | const untaggedReleaseUrl = `https://gitea.io/${owner}/${repo}/releases/untagged-123`; 210 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 211 | const assetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${nextRelease.version}/my-software-v1.0.0.tar.gz`; 212 | const releaseId = 1; 213 | 214 | const gitea = authenticate(env) 215 | .post(`/repos/${owner}/${repo}/releases`, { 216 | tag_name: nextRelease.gitTag, 217 | name: nextRelease.name, 218 | body: nextRelease.notes, 219 | draft: true, 220 | prerelease: false, 221 | }) 222 | .reply(200, { url: untaggedReleaseUrl, id: releaseId}) 223 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, {draft: false}) 224 | .reply(200, {url: releaseUrl}); 225 | 226 | const giteaUpload = upload(env) 227 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('Version 1.0.0 of my software')}`) 228 | .reply(200, {browser_download_url: assetUrl}); 229 | 230 | const result = await publish(pluginConfig, { 231 | cwd, 232 | env, 233 | options, 234 | branch: {type: 'release', main: true}, 235 | nextRelease, 236 | logger: t.context.logger, 237 | }); 238 | 239 | t.is(result.url, releaseUrl); 240 | t.true(t.context.log.calledWith('Published Gitea release: %s', releaseUrl)); 241 | t.true(t.context.log.calledWith('Published file %s', assetUrl)); 242 | t.true(gitea.isDone()); 243 | t.true(giteaUpload.isDone()); 244 | }); 245 | 246 | test.serial('Publish a release with an array of missing assets', async t => { 247 | const owner = 'test_user'; 248 | const repo = 'test_repo'; 249 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 250 | const emptyDirectory = tempy.directory(); 251 | const pluginConfig = {assets: [emptyDirectory, {path: 'missing.txt', name: 'missing.txt'}]}; 252 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 253 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 254 | const untaggedReleaseUrl = `https://gitea.io/${owner}/${repo}/releases/untagged-123`; 255 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${nextRelease.version}`; 256 | const releaseId = 1; 257 | 258 | const gitea = authenticate(env) 259 | .post(`/repos/${owner}/${repo}/releases`, { 260 | tag_name: nextRelease.gitTag, 261 | name: nextRelease.name, 262 | body: nextRelease.notes, 263 | draft: true, 264 | prerelease: false, 265 | }) 266 | .reply(200, {url: untaggedReleaseUrl, id: releaseId}) 267 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, {draft: false}) 268 | .reply(200, {url: releaseUrl}); 269 | 270 | const result = await publish(pluginConfig, { 271 | cwd, 272 | env, 273 | options, 274 | branch: {type: 'release', main: true}, 275 | nextRelease, 276 | logger: t.context.logger, 277 | }); 278 | 279 | t.is(result.url, releaseUrl); 280 | t.true(t.context.log.calledWith('Published Gitea release: %s', releaseUrl)); 281 | t.true(t.context.error.calledWith('The asset %s cannot be read, and will be ignored.', 'missing.txt')); 282 | t.true(t.context.error.calledWith('The asset %s is not a file, and will be ignored.', emptyDirectory)); 283 | t.true(gitea.isDone()); 284 | }); 285 | 286 | -------------------------------------------------------------------------------- /test/verify.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import nock from 'nock'; 3 | import {stub} from 'sinon'; 4 | import {authenticate} from './helpers/_mock-gitea'; 5 | 6 | /* eslint camelcase: ["error", {properties: "never"}] */ 7 | 8 | const verify = require('../lib/verify'); 9 | 10 | test.beforeEach(t => { 11 | // Mock logger 12 | t.context.log = stub(); 13 | t.context.error = stub(); 14 | t.context.logger = {log: t.context.log, error: t.context.error}; 15 | }); 16 | 17 | test.afterEach.always(() => { 18 | // Clear nock 19 | nock.cleanAll(); 20 | }); 21 | 22 | test.serial('Verify package, token and repository access', async t => { 23 | const owner = 'test_user'; 24 | const repo = 'test_repo'; 25 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 26 | const assets = [{path: 'lib/file.js'}, 'file.js']; 27 | const gitea = authenticate(env) 28 | .get(`/repos/${owner}/${repo}`) 29 | .reply(200, {permissions: {push: true}}); 30 | 31 | await t.notThrowsAsync( 32 | verify( 33 | {assets}, 34 | {env, options: {repositoryUrl: `git+https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger} 35 | ) 36 | ); 37 | t.true(gitea.isDone()); 38 | }); 39 | 40 | test.serial( 41 | 'Verify package, token and repository access with "asset" set to "null"', 42 | async t => { 43 | const owner = 'test_user'; 44 | const repo = 'test_repo'; 45 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 46 | const assets = null; 47 | const gitea = authenticate(env) 48 | .get(`/repos/${owner}/${repo}`) 49 | .reply(200, {permissions: {push: true}}); 50 | await t.notThrowsAsync( 51 | verify( 52 | {assets}, 53 | {env, options: {repositoryUrl: `git+https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger} 54 | ) 55 | ); 56 | t.true(gitea.isDone()); 57 | } 58 | ); 59 | 60 | test.serial('Verify package, token and repository with environment variables', async t => { 61 | const owner = 'test_user'; 62 | const repo = 'test_repo'; 63 | const env = { 64 | GITEA_URL: 'https://gitea.io', 65 | GITEA_TOKEN: 'gitea_token', 66 | GITEA_PREFIX: 'prefix', 67 | }; 68 | const gitea = authenticate(env) 69 | .get(`/repos/${owner}/${repo}`) 70 | .reply(200, {permissions: {push: true}}); 71 | await t.notThrowsAsync( 72 | verify({}, {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger}) 73 | ); 74 | t.true(gitea.isDone()); 75 | t.assert(t.context.log.args[0], 'Verify Gitea authentication (https://gitea.io/prefix)'); 76 | }); 77 | 78 | test.serial('Verify package, token and repository access with alternative environment varialbes', async t => { 79 | const owner = 'test_user'; 80 | const repo = 'test_repo'; 81 | const env = { 82 | GITEA_URL: 'https://gitea.io', 83 | GITEA_TOKEN: 'gitea_token', 84 | GITEA_PREFIX: 'prefix', 85 | }; 86 | const gitea = authenticate(env) 87 | .get(`/repos/${owner}/${repo}`) 88 | .reply(200, {permissions: {push: true}}); 89 | await t.notThrowsAsync( 90 | verify({}, {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger}) 91 | ); 92 | t.true(gitea.isDone()); 93 | }); 94 | 95 | 96 | test.serial('Verify "assets" is a String', async t => { 97 | const owner = 'test_user'; 98 | const repo = 'test_repo'; 99 | const env = {GITEA_URL: 'https://gitea.io',GITEA_TOKEN: 'gitea_token'}; 100 | const assets = 'file2.js'; 101 | const gitea = authenticate(env) 102 | .get(`/repos/${owner}/${repo}`) 103 | .reply(200, {permissions: {push: true}}); 104 | 105 | await t.notThrowsAsync( 106 | verify( 107 | {assets}, 108 | {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger} 109 | ) 110 | ); 111 | 112 | t.true(gitea.isDone()); 113 | }); 114 | 115 | test.serial('Verify "assets" is an Object with a path property', async t => { 116 | const owner = 'test_user'; 117 | const repo = 'test_repo'; 118 | const env = {GITEA_URL: 'https://gitea.io',GITEA_TOKEN: 'gitea_token'}; 119 | const assets = {path: 'file2.js'}; 120 | const gitea = authenticate(env) 121 | .get(`/repos/${owner}/${repo}`) 122 | .reply(200, {permissions: {push: true}}); 123 | 124 | await t.notThrowsAsync( 125 | verify( 126 | {assets}, 127 | {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger} 128 | ) 129 | ); 130 | 131 | t.true(gitea.isDone()); 132 | }); 133 | 134 | test.serial('Verify "assets" is an Array of Object with a path property', async t => { 135 | const owner = 'test_user'; 136 | const repo = 'test_repo'; 137 | const env = {GITEA_URL: 'https://gitea.io',GITEA_TOKEN: 'gitea_token'}; 138 | const assets = [{path: 'file1.js'}, {path: 'file2.js'}]; 139 | const gitea = authenticate(env) 140 | .get(`/repos/${owner}/${repo}`) 141 | .reply(200, {permissions: {push: true}}); 142 | 143 | await t.notThrowsAsync( 144 | verify( 145 | {assets}, 146 | {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger} 147 | ) 148 | ); 149 | 150 | t.true(gitea.isDone()); 151 | }); 152 | 153 | test.serial('Verify "assets" is an Array of glob Arrays', async t => { 154 | const owner = 'test_user'; 155 | const repo = 'test_repo'; 156 | const env = {GITEA_URL: 'https://gitea.io',GITEA_TOKEN: 'gitea_token'}; 157 | const assets = [['dist/**', '!**/*.js'], 'file2.js']; 158 | const gitea = authenticate(env) 159 | .get(`/repos/${owner}/${repo}`) 160 | .reply(200, {permissions: {push: true}}); 161 | 162 | await t.notThrowsAsync( 163 | verify( 164 | {assets}, 165 | {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger} 166 | ) 167 | ); 168 | 169 | t.true(gitea.isDone()); 170 | }); 171 | 172 | test.serial('Verify "assets" is an Array of Object with a glob Arrays in path property', async t => { 173 | const owner = 'test_user'; 174 | const repo = 'test_repo'; 175 | const env = {GITEA_URL: 'https://gitea.io',GITEA_TOKEN: 'gitea_token'}; 176 | const assets = [{path: ['dist/**', '!**/*.js']}, {path: 'file2.js'}]; 177 | const gitea = authenticate(env) 178 | .get(`/repos/${owner}/${repo}`) 179 | .reply(200, {permissions: {push: true}}); 180 | 181 | await t.notThrowsAsync( 182 | verify( 183 | {assets}, 184 | {env, options: {repositoryUrl: `git@gitea.io:${owner}/${repo}.git`}, logger: t.context.logger} 185 | ) 186 | ); 187 | 188 | t.true(gitea.isDone()); 189 | }); 190 | 191 | test.serial('Throw SemanticReleaseError for invalid token', async t => { 192 | const owner = 'test_user'; 193 | const repo = 'test_repo'; 194 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 195 | const gitea = authenticate(env) 196 | .get(`/repos/${owner}/${repo}`) 197 | .reply(401); 198 | 199 | const [error, ...errors] = await t.throwsAsync( 200 | verify({}, {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger}) 201 | ); 202 | 203 | t.is(errors.length, 0); 204 | t.is(error.name, 'SemanticReleaseError'); 205 | t.is(error.code, 'EINVALIDGITEATOKEN'); 206 | t.true(gitea.isDone()); 207 | }); 208 | 209 | test('Throw SemanticReleaseError if no Gitea URL is provided', async t => { 210 | const env = {GITEA_TOKEN: 'gitea_token'}; 211 | 212 | const [error, ...errors] = await t.throwsAsync( 213 | verify({}, {env, options: {repositoryUrl: 'https://gitea.io/${owner}/${repo}.git'}, logger: t.context.logger}) 214 | ); 215 | 216 | t.is(errors.length, 0); 217 | t.is(error.name, 'SemanticReleaseError'); 218 | t.is(error.code, 'ENOGITEAURL'); 219 | }); 220 | 221 | test('Throw SemanticReleaseError for invalid repositoryUrl', async t => { 222 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 223 | 224 | const [error, ...errors] = await t.throwsAsync( 225 | verify({}, {env, options: {repositoryUrl: 'invalid_url'}, logger: t.context.logger}) 226 | ); 227 | 228 | t.is(errors.length, 0); 229 | t.is(error.name, 'SemanticReleaseError'); 230 | t.is(error.code, 'EINVALIDGITEAURL'); 231 | }); 232 | 233 | test.serial("Throw SemanticReleaseError if token doesn't have the push permission on the repository", async t => { 234 | const owner = 'test_user'; 235 | const repo = 'test_repo'; 236 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 237 | const gitea = authenticate(env) 238 | .get(`/repos/${owner}/${repo}`) 239 | .reply(200, {permissions: {push: false}}); 240 | 241 | const [error, ...errors] = await t.throwsAsync( 242 | verify({}, {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger}) 243 | ); 244 | 245 | t.is(errors.length, 0); 246 | t.is(error.name, 'SemanticReleaseError'); 247 | t.is(error.code, 'EGITEANOPERMISSION'); 248 | t.true(gitea.isDone()); 249 | }); 250 | 251 | test.serial("Throw SemanticReleaseError if the repository doesn't exist", async t => { 252 | const owner = 'test_user'; 253 | const repo = 'test_repo'; 254 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 255 | const gitea = authenticate(env) 256 | .get(`/repos/${owner}/${repo}`) 257 | .reply(404); 258 | 259 | const [error, ...errors] = await t.throwsAsync( 260 | verify({}, {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger}) 261 | ); 262 | 263 | t.is(errors.length, 0); 264 | t.is(error.name, 'SemanticReleaseError'); 265 | t.is(error.code, 'EMISSINGREPO'); 266 | t.true(gitea.isDone()); 267 | }); 268 | 269 | test.serial("Throw error if Gitea return any other errors", async t => { 270 | const owner = 'test_user'; 271 | const repo = 'test_repo'; 272 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 273 | const gitea = authenticate(env) 274 | .get(`/repos/${owner}/${repo}`) 275 | .reply(500); 276 | 277 | await t.throwsAsync( 278 | verify({}, {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger}) 279 | ); 280 | t.true(gitea.isDone()); 281 | }); 282 | 283 | test.serial('Throw SemanticReleaseError if "assets" option is not a String or an Array of Objects', async t => { 284 | const owner = 'test_user'; 285 | const repo = 'test_repo'; 286 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 287 | const assets = 42; 288 | const gitea = authenticate(env) 289 | .get(`/repos/${owner}/${repo}`) 290 | .reply(200, {permissions: {push: true}}); 291 | 292 | const [error, ...errors] = await t.throwsAsync( 293 | verify( 294 | {assets}, 295 | {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger} 296 | ) 297 | ); 298 | 299 | t.is(errors.length, 0); 300 | t.is(error.name, 'SemanticReleaseError'); 301 | t.is(error.code, 'EINVALIDASSETS'); 302 | t.true(gitea.isDone()); 303 | }); 304 | 305 | test.serial('Throw SemanticReleaseError if "assets" option is an Array with invalid elements', async t => { 306 | const owner = 'test_user'; 307 | const repo = 'test_repo'; 308 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 309 | const assets = ['file.js', 42]; 310 | const gitea = authenticate(env) 311 | .get(`/repos/${owner}/${repo}`) 312 | .reply(200, {permissions: {push: true}}); 313 | 314 | const [error, ...errors] = await t.throwsAsync( 315 | verify( 316 | {assets}, 317 | {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger} 318 | ) 319 | ); 320 | 321 | t.is(errors.length, 0); 322 | t.is(error.name, 'SemanticReleaseError'); 323 | t.is(error.code, 'EINVALIDASSETS'); 324 | t.true(gitea.isDone()); 325 | }); 326 | 327 | test.serial('Throw SemanticReleaseError if "assets" option is an Object missing the "path" property', async t => { 328 | const owner = 'test_user'; 329 | const repo = 'test_repo'; 330 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 331 | const assets = {name: 'file.js'}; 332 | const gitea = authenticate(env) 333 | .get(`/repos/${owner}/${repo}`) 334 | .reply(200, {permissions: {push: true}}); 335 | 336 | const [error, ...errors] = await t.throwsAsync( 337 | verify( 338 | {assets}, 339 | {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger} 340 | ) 341 | ); 342 | 343 | t.is(errors.length, 0); 344 | t.is(error.name, 'SemanticReleaseError'); 345 | t.is(error.code, 'EINVALIDASSETS'); 346 | t.true(gitea.isDone()); 347 | }); 348 | 349 | test.serial( 350 | 'Throw SemanticReleaseError if "assets" option is an Array with objects missing the "path" property', 351 | async t => { 352 | const owner = 'test_user'; 353 | const repo = 'test_repo'; 354 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 355 | const assets = [{path: 'lib/file.js'}, {name: 'file.js'}]; 356 | const gitea = authenticate(env) 357 | .get(`/repos/${owner}/${repo}`) 358 | .reply(200, {permissions: {push: true}}); 359 | 360 | const [error, ...errors] = await t.throwsAsync( 361 | verify( 362 | {assets}, 363 | {env, options: {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}, logger: t.context.logger} 364 | ) 365 | ); 366 | 367 | t.is(errors.length, 0); 368 | t.is(error.name, 'SemanticReleaseError'); 369 | t.is(error.code, 'EINVALIDASSETS'); 370 | t.true(gitea.isDone()); 371 | } 372 | ); 373 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | import {escape} from 'querystring'; 2 | import test from 'ava'; 3 | import nock from 'nock'; 4 | import {stub} from 'sinon'; 5 | import proxyquire from 'proxyquire'; 6 | import clearModule from 'clear-module'; 7 | import SemanticReleaseError from '@semantic-release/error'; 8 | import {authenticate, upload} from './helpers/_mock-gitea'; 9 | 10 | const cwd = 'test/fixtures/files'; 11 | const client = require('../lib/get-client'); 12 | 13 | test.beforeEach(t => { 14 | // Clear npm cache to refresh the module state 15 | clearModule('..'); 16 | t.context.m = proxyquire('..', { 17 | './lib/verify': proxyquire('../lib/verify', {'./get-client': client}), 18 | './lib/publish': proxyquire('../lib/publish', {'./get-client': client}), 19 | }); 20 | // Stub the logger 21 | t.context.log = stub(); 22 | t.context.error = stub(); 23 | t.context.logger = {log: t.context.log, error: t.context.error}; 24 | }); 25 | 26 | test.afterEach.always(() => { 27 | // Clear nock 28 | nock.cleanAll(); 29 | }); 30 | 31 | test.serial('Verify Gitea auth', async t => { 32 | const owner = 'test_user'; 33 | const repo = 'test_repo'; 34 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 35 | const options = {repositoryUrl: `git+https://gitea.io/${owner}/${repo}.git`}; 36 | const gitea = authenticate(env) 37 | .get(`/repos/${owner}/${repo}`) 38 | .reply(200, {permissions: {push: true}}); 39 | 40 | await t.notThrowsAsync(t.context.m.verifyConditions({}, {cwd, env, options, logger: t.context.logger})); 41 | 42 | t.true(gitea.isDone()); 43 | }); 44 | 45 | test.serial('Verify Gitea auth with publish options', async t => { 46 | const owner = 'test_user'; 47 | const repo = 'test_repo'; 48 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 49 | const options = { 50 | publish: {path: '@saithodev/semantic-release-gitea'}, 51 | repositoryUrl: `git+https://gitea.io/${owner}/${repo}.git`, 52 | }; 53 | const gitea = authenticate(env) 54 | .get(`/repos/${owner}/${repo}`) 55 | .reply(200, {permissions: {push: true}}); 56 | 57 | await t.notThrowsAsync(t.context.m.verifyConditions({}, {cwd, env, options, logger: t.context.logger})); 58 | 59 | t.true(gitea.isDone()); 60 | }); 61 | 62 | test.serial('Verify Gitea auth and assets config', async t => { 63 | const owner = 'test_user'; 64 | const repo = 'test_repo'; 65 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 66 | const assets = [ 67 | {path: 'lib/file.js'}, 68 | 'file.js', 69 | ['dist/**'], 70 | ['dist/**', '!dist/*.js'], 71 | {path: ['dist/**', '!dist/*.js']}, 72 | ]; 73 | const options = { 74 | publish: [{path: '@semantic-release/npm'}], 75 | repositoryUrl: `git+https://gitea.io/${owner}/${repo}.git`, 76 | }; 77 | const gitea = authenticate(env) 78 | .get(`/repos/${owner}/${repo}`) 79 | .reply(200, {permissions: {push: true}}); 80 | 81 | await t.notThrowsAsync(t.context.m.verifyConditions({assets}, {cwd, env, options, logger: t.context.logger})); 82 | 83 | t.true(gitea.isDone()); 84 | }); 85 | 86 | test.serial('Throw SemanticReleaseError if invalid config', async t => { 87 | const env = {}; 88 | const assets = [{wrongProperty: 'lib/file.js'}]; 89 | const options = { 90 | publish: [ 91 | {path: '@semantic-release/npm'}, 92 | {path: '@saithodev/semantic-release-gitea', assets}, 93 | ], 94 | repositoryUrl: 'invalid_url', 95 | }; 96 | 97 | const errors = [ 98 | ...(await t.throwsAsync(t.context.m.verifyConditions({}, {cwd, env, options, logger: t.context.logger}))), 99 | ]; 100 | 101 | t.is(errors[0].name, 'SemanticReleaseError'); 102 | t.is(errors[0].code, 'EINVALIDASSETS'); 103 | t.is(errors[1].name, 'SemanticReleaseError'); 104 | t.is(errors[1].code, 'ENOGITEAURL'); 105 | t.is(errors[2].name, 'SemanticReleaseError'); 106 | t.is(errors[2].code, 'ENOGITEATOKEN'); 107 | }); 108 | 109 | test.serial('Publish a release with an array of assets', async t => { 110 | const owner = 'test_user'; 111 | const repo = 'test_repo'; 112 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 113 | const assets = [ 114 | {path: ['upload.txt'], name: 'upload_file_name.txt'}, 115 | {path: ['upload_other.txt'], name: 'other_file.txt', label: 'Other File'}, 116 | ]; 117 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 118 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 119 | const releaseId = 1; 120 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 121 | const assetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${releaseId}/upload.txt`; 122 | const otherAssetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${releaseId}/other_file.txt`; 123 | const gitea = authenticate(env) 124 | .get(`/repos/${owner}/${repo}`) 125 | .reply(200, {permissions: {push: true}}) 126 | .post(`/repos/${owner}/${repo}/releases`, { 127 | tag_name: nextRelease.gitTag, 128 | name: nextRelease.name, 129 | body: nextRelease.notes, 130 | draft: true, 131 | prerelease: false, 132 | }) 133 | .reply(200, {url: releaseUrl, id: releaseId}) 134 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, {draft: false}) 135 | .reply(200, {url: releaseUrl}); 136 | const giteaUpload = upload(env) 137 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('upload_file_name.txt')}`) 138 | .reply(200, {browser_download_url: assetUrl}) 139 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('Other File')}`) 140 | .reply(200, {browser_download_url: otherAssetUrl}); 141 | 142 | const result = await t.context.m.publish( 143 | {assets}, 144 | {cwd, env, options, branch: {type: 'release', main: true}, nextRelease, logger: t.context.logger} 145 | ); 146 | 147 | t.is(result.url, releaseUrl); 148 | t.regex(t.context.log.args[0][0], new RegExp('^Verify Gitea authentication \(.*\)$')); 149 | t.true(t.context.log.calledWith('Published file %s', otherAssetUrl)); 150 | t.true(t.context.log.calledWith('Published file %s', assetUrl)); 151 | t.true(t.context.log.calledWith('Published Gitea release: %s', releaseUrl)); 152 | t.true(gitea.isDone()); 153 | t.true(giteaUpload.isDone()); 154 | }); 155 | 156 | test.serial('Publish a release with release information in assets', async t => { 157 | const owner = 'test_user'; 158 | const repo = 'test_repo'; 159 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 160 | const assets = [ 161 | { 162 | path: ['upload.txt'], 163 | name: `file_with_release_\${nextRelease.gitTag}_in_filename.txt`, 164 | label: `File with release \${nextRelease.gitTag} in label`, 165 | }, 166 | ]; 167 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 168 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 169 | const releaseId = 1; 170 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 171 | const assetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${releaseId}/file_with_release_v1.0.0_in_filename.txt`; 172 | const gitea = authenticate(env) 173 | .get(`/repos/${owner}/${repo}`) 174 | .reply(200, {permissions: {push: true}}) 175 | .post(`/repos/${owner}/${repo}/releases`, { 176 | tag_name: nextRelease.gitTag, 177 | target_commitish: options.branch, 178 | name: nextRelease.gitTag, 179 | body: nextRelease.notes, 180 | draft: true, 181 | }) 182 | .reply(200, {url: releaseUrl, id: releaseId}) 183 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 184 | draft: false, 185 | }) 186 | .reply(200, {url: releaseUrl}); 187 | const giteaUpload = upload(env) 188 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('File with release v1.0.0 in label')}`) 189 | .reply(200, {browser_download_url: assetUrl}); 190 | 191 | const result = await t.context.m.publish( 192 | {assets}, 193 | {cwd, env, options, branch: {type: 'release'}, nextRelease, logger: t.context.logger} 194 | ); 195 | 196 | t.is(result.url, releaseUrl); 197 | t.regex(t.context.log.args[0][0], new RegExp('^Verify Gitea authentication \(.*\)$')); 198 | t.true(t.context.log.calledWith('Published file %s', assetUrl)); 199 | t.true(t.context.log.calledWith('Published Gitea release: %s', releaseUrl)); 200 | t.true(gitea.isDone()); 201 | t.true(giteaUpload.isDone()); 202 | }); 203 | 204 | test.serial('Update a release', async t => { 205 | const owner = 'test_user'; 206 | const repo = 'test_repo'; 207 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 208 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 209 | const options = {repositoryUrl: `https://gitea.io/${owner}/${repo}.git`}; 210 | const releaseId = 1; 211 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 212 | 213 | const gitea = authenticate(env) 214 | .get(`/repos/${owner}/${repo}`) 215 | .reply(200, {permissions: {push: true}}) 216 | .get(`/repos/${owner}/${repo}/releases?page=1`) 217 | .reply(200, [{id: releaseId, tag_name: nextRelease.gitTag}]) 218 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 219 | tag_name: nextRelease.gitTag, 220 | name: nextRelease.name, 221 | prerelease: false, 222 | }) 223 | .reply(200, {url: releaseUrl}); 224 | 225 | const result = await t.context.m.addChannel( 226 | {}, 227 | {cwd, env, options, branch: {type: 'release', main: true}, nextRelease, logger: t.context.logger} 228 | ); 229 | 230 | t.is(result.url, releaseUrl); 231 | t.regex(t.context.log.args[0][0], new RegExp('^Verify Gitea authentication \(.*\)$')); 232 | t.deepEqual(t.context.log.args[1], ['Updated Gitea release: %s', releaseUrl]); 233 | t.true(gitea.isDone()); 234 | }); 235 | 236 | test.serial('Verify and release', async t => { 237 | const owner = 'test_user'; 238 | const repo = 'test_repo'; 239 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 240 | const assets = ['upload.txt', {path: 'upload_other.txt', name: 'other_file.txt', label: 'Other File'}]; 241 | const options = { 242 | publish: [{path: '@semantic-release/npm'}, {path: '@saithodev/semantic-release-gitea', assets}], 243 | repositoryUrl: `https://gitea.io/${owner}/${repo}.git`, 244 | }; 245 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 246 | const releaseId = 1; 247 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 248 | const assetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${releaseId}/upload.txt`; 249 | const otherAssetUrl = `https://gitea.io/${owner}/${repo}/releases/download/${releaseId}/other_file.txt`; 250 | const github = authenticate(env) 251 | .get(`/repos/${owner}/${repo}`) 252 | .reply(200, {permissions: {push: true}}) 253 | .post(`/repos/${owner}/${repo}/releases`, { 254 | tag_name: nextRelease.gitTag, 255 | name: nextRelease.name, 256 | body: nextRelease.notes, 257 | draft: true, 258 | prerelease: false, 259 | }) 260 | .reply(200, {url: releaseUrl, id: releaseId}) 261 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, {draft: false}) 262 | .reply(200, {url: releaseUrl}); 263 | const giteaUpload = upload(env) 264 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('upload.txt')}`) 265 | .reply(200, {browser_download_url: assetUrl}) 266 | .post(`/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${escape('Other File')}`) 267 | .reply(200, {browser_download_url: otherAssetUrl}); 268 | 269 | await t.notThrowsAsync(t.context.m.verifyConditions({}, {cwd, env, options, logger: t.context.logger})); 270 | await t.context.m.publish( 271 | {assets}, 272 | {cwd, env, options, branch: {type: 'release', main: true}, nextRelease, logger: t.context.logger} 273 | ); 274 | 275 | t.regex(t.context.log.args[0][0], new RegExp('^Verify Gitea authentication \(.*\)$')); 276 | t.true(t.context.log.calledWith('Published file %s', otherAssetUrl)); 277 | t.true(t.context.log.calledWith('Published file %s', assetUrl)); 278 | t.true(t.context.log.calledWith('Published Gitea release: %s', releaseUrl)); 279 | t.true(github.isDone()); 280 | t.true(giteaUpload.isDone()); 281 | }); 282 | 283 | test.serial('Verify and update release', async t => { 284 | const owner = 'test_user'; 285 | const repo = 'test_repo'; 286 | const env = {GITEA_URL: 'https://gitea.io', GITEA_TOKEN: 'gitea_token'}; 287 | const options = { 288 | publish: [{path: '@semantic-release/npm'}, {path: '@saithodev/semantic-release-gitea'}], 289 | repositoryUrl: `https://gitea.io/${owner}/${repo}.git`, 290 | }; 291 | const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'}; 292 | const releaseId = 1; 293 | const releaseUrl = `https://gitea.io/${owner}/${repo}/releases/${releaseId}`; 294 | const gitea = authenticate(env) 295 | .get(`/repos/${owner}/${repo}`) 296 | .reply(200, {permissions: {push: true}}) 297 | .get(`/repos/${owner}/${repo}/releases?page=1`) 298 | .reply(200, [{id: releaseId, tag_name: nextRelease.gitTag}]) 299 | .patch(`/repos/${owner}/${repo}/releases/${releaseId}`, { 300 | tag_name: nextRelease.gitTag, 301 | name: nextRelease.name, 302 | prerelease: false, 303 | }) 304 | .reply(200, {url: releaseUrl}); 305 | 306 | await t.notThrowsAsync(t.context.m.verifyConditions({}, {cwd, env, options, logger: t.context.logger})); 307 | await t.context.m.addChannel( 308 | {}, 309 | {cwd, env, branch: {type: 'release', main: true}, nextRelease, options, logger: t.context.logger} 310 | ); 311 | 312 | t.regex(t.context.log.args[0][0], new RegExp('^Verify Gitea authentication \(.*\)$')); 313 | t.deepEqual(t.context.log.args[1], ['Updated Gitea release: %s', releaseUrl]); 314 | t.true(gitea.isDone()); 315 | }); 316 | --------------------------------------------------------------------------------