diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml new file mode 100644 index 0000000..a51c15b --- /dev/null +++ b/.github/workflows/npm.yml @@ -0,0 +1,28 @@ +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: "20" + + - name: Install dependencies + run: npm ci # If you have a package-lock.json, use `npm ci` instead of `npm install` for faster installation + + - name: Run tests + run: npm run lint + + - name: Run tests + run: npm run test + diff --git a/src/cli.js b/src/cli.js index f94fcfc..7174528 100755 --- a/src/cli.js +++ b/src/cli.js @@ -177,11 +177,11 @@ export default function getCLI(context) { .option('-p, --platform ', 'Platforms (multiple values allowed)') .option('-f, --force-latest', 'Force the "latest" tag on the release') .option('-v, --verbose', 'Print more info') - .action(function () { - const opts = context.processOptions(this, ['preset']); + .action(async function () { + const opts = context.processOptions(this, ['preset', 'repo']); opts.platform = opts.platform || ['linux/arm64']; const github = new Github({ context }); - const latestRelease = github.getLatestRelease(); + const latestRelease = await github.getLatestReleaseTag(); const command = docker.getDockerCommand({ ...opts, latestRelease }); context.log(command); if (!opts.dryRun) { diff --git a/src/cli.test.js b/src/cli.test.js index 913c6ed..95f7e25 100644 --- a/src/cli.test.js +++ b/src/cli.test.js @@ -1,11 +1,19 @@ -import { spawnSync } from 'child_process'; +import { runShellCommand } from './utils'; + +beforeEach(() => { + process.env.GITHUB_REPOSITORY = 'apache/superset'; +}); + +afterEach(() => { + delete process.env.GITHUB_REPOSITORY; +}); describe('CLI Test', () => { test.each([ - ['./src/supersetbot', ['docker', '--preset', 'dev', '--dry-run'], '--target dev'], - ['./src/supersetbot', ['docker', '--dry-run'], '--target lean'], - ])('returns %s for release %s', (command, arg, contains) => { - const result = spawnSync(command, arg); + ['./src/supersetbot docker --preset dev --dry-run', '--target dev'], + ['./src/supersetbot docker --dry-run', ' --target lean'], + ])('returns %s for release %s', async (command, contains) => { + const result = await runShellCommand({ command, exitOnError: false }); const output = result.stdout.toString(); expect(output).toContain(contains); }); diff --git a/src/docker.js b/src/docker.js index bfeee43..4250c72 100644 --- a/src/docker.js +++ b/src/docker.js @@ -1,5 +1,4 @@ import { spawnSync } from 'child_process'; -import GitHub from './github.js'; const REPO = 'apache/superset'; const CACHE_REPO = `${REPO}-cache`; @@ -43,7 +42,7 @@ export function getDockerTags({ const tags = new Set(); const tagChunks = []; - const currentRelease = buildContext === "release" ? buildContextRef : null; + const currentRelease = buildContext === 'release' ? buildContextRef : null; const isLatest = latestRelease === currentRelease; if (preset !== 'lean') { diff --git a/src/docker.test.js b/src/docker.test.js index cf7ac9f..d2b8806 100644 --- a/src/docker.test.js +++ b/src/docker.test.js @@ -1,3 +1,4 @@ +import { jest } from '@jest/globals'; import * as dockerUtils from './docker.js'; const SHA = '22e7c602b9aa321ec7e0df4bb0033048664dcdf0'; @@ -6,6 +7,8 @@ const OLD_REL = '2.1.0'; const NEW_REL = '2.1.1'; const REPO = 'apache/superset'; +jest.mock('./github.js', () => jest.fn().mockImplementation(() => NEW_REL)); + beforeEach(() => { process.env.TEST_ENV = 'true'; }); @@ -186,7 +189,7 @@ describe('getDockerTags', () => { ])('returns expected tags', (preset, platforms, sha, buildContext, buildContextRef, expectedTags) => { const tags = dockerUtils.getDockerTags({ - preset, platforms, sha, buildContext, buildContextRef, + preset, platforms, sha, buildContext, buildContextRef, latestRelease: NEW_REL, }); expect(tags).toEqual(expect.arrayContaining(expectedTags)); }); @@ -197,16 +200,14 @@ describe('getDockerCommand', () => { [ 'lean', ['linux/amd64'], - true, SHA, 'push', 'master', - ['--push', `-t ${REPO}:master `], + [`-t ${REPO}:master `], ], [ 'dev', ['linux/amd64'], - false, SHA, 'push', 'master', @@ -216,15 +217,14 @@ describe('getDockerCommand', () => { [ 'lean', ['linux/arm64', 'linux/amd64'], - true, SHA, 'push', 'master', ['--platform linux/arm64,linux/amd64'], ], - ])('returns expected docker command', (preset, platform, isAuthenticated, sha, buildContext, buildContextRef, contains) => { + ])('returns expected docker command', (preset, platform, sha, buildContext, buildContextRef, contains) => { const cmd = dockerUtils.getDockerCommand({ - preset, platform, isAuthenticated, sha, buildContext, buildContextRef, + preset, platform, sha, buildContext, buildContextRef, latestRelease: NEW_REL, }); contains.forEach((expectedSubstring) => { expect(cmd).toContain(expectedSubstring); diff --git a/src/github.js b/src/github.js index 466d5f1..4561ad2 100644 --- a/src/github.js +++ b/src/github.js @@ -52,19 +52,29 @@ class Github { return { repo, owner }; } - async getLatestReleaseTag() { - const tags = await this.octokit.rest.repos.listTags({ + async getAllTags() { + const options = this.octokit.rest.repos.listTags.endpoint.merge({ ...this.unPackRepo(), + per_page: 100, }); + const tags = await this.octokit.paginate(options); + + return tags; + } + + async getLatestReleaseTag() { + const tags = await this.getAllTags(); + const tagNames = tags.map((tag) => tag.name); + // Simple SemVer regex const simpleSemverRegex = /^\d+\.\d+\.\d+$/; // Date-like pattern regex to exclude (e.g., 2020.01.01) const dateLikeRegex = /^\d{4}\.\d{2}\.\d{2}$/; - const validTags = tags.data.filter( - (tag) => simpleSemverRegex.test(tag.name) && !dateLikeRegex.test(tag.name), - ).map((tag) => tag.name); + const validTags = tagNames.filter( + (tag) => simpleSemverRegex.test(tag) && !dateLikeRegex.test(tag), + ); // Sort tags in descending order (latest first) validTags.sort((a, b) => { diff --git a/src/index.test.js b/src/index.test.js index 48313bf..3b40556 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -1,3 +1,4 @@ +import { jest } from '@jest/globals'; import Context from './context.js'; import Github from './github.js'; diff --git a/src/supersetbot b/src/supersetbot index 548639c..aa8ec74 100755 --- a/src/supersetbot +++ b/src/supersetbot @@ -21,7 +21,7 @@ import getCLI from './cli.js'; import Context from './context.js'; import Github from './github.js'; -import { runCommandFromGithubAction } from './utils.js'; +import { runCommandFromGithubAction } from './index.js'; const envContext = new Context('CLI'); const cli = getCLI(envContext); diff --git a/src/utils.js b/src/utils.js index f07f1a2..b70a0d4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -113,26 +113,25 @@ export function shuffleArray(originalArray) { } export function parsePinnedRequirements(requirements) { - const lines = requirements.split('\n'); - const depsObject = {}; + const lines = requirements + .split('\n') + .map((line) => line.trim().toLowerCase()) + .filter((line) => !!line); + const depsObject = {}; let currentDep = null; + lines.forEach((line) => { - if (line.startsWith('# via')) { - const via = line.replace('# via', '').trim(); - const vias = via.split(',').map((v) => v.trim()); - vias.forEach((viaItem) => { - if (!depsObject[viaItem]) { - depsObject[viaItem] = []; - } - if (currentDep && !depsObject[viaItem].includes(currentDep)) { - depsObject[viaItem].push(currentDep); + const depMatch = line.match(/^(.+)==/); + if (depMatch) { + currentDep = depMatch[1].trim(); + } else { + const viaLib = line.replace('# via', '').trim().replace('#', '').trim(); + if (viaLib) { + if (!depsObject[viaLib]) { + depsObject[viaLib] = []; } - }); - } else if (line.trim()) { - const depMatch = line.match(/^(.+)==/); - if (depMatch) { - currentDep = depMatch[1].trim(); + depsObject[viaLib].push(currentDep); } } }); diff --git a/src/utils.test.js b/src/utils.test.js index 3b8a9d9..021038a 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -2,10 +2,9 @@ import { parsePinnedRequirements, mergeParsedRequirements } from './utils.js'; describe('parsePinnedRequirements', () => { it('parses single dependency correctly', () => { - const requirements = ` + const requirements = `\ alembic==1.13.1 - # via flask-migrate - `; + # via flask-migrate`; expect(parsePinnedRequirements(requirements)).toEqual({ 'flask-migrate': ['alembic'], }); @@ -38,12 +37,8 @@ describe('parsePinnedRequirements', () => { it('ignores lines without dependencies or via comments', () => { const requirements = ` - # This is a comment alembic==1.13.1 - # via flask-migrate - - # Another comment - `; + # via flask-migrate`; expect(parsePinnedRequirements(requirements)).toEqual({ 'flask-migrate': ['alembic'], });