Skip to content

Commit

Permalink
Initial and highly untested node.js implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
nutjob4life committed Feb 8, 2024
1 parent fe57dcf commit 7ecd0a4
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 4 deletions.
258 changes: 258 additions & 0 deletions src/pds/roundup/_nodejs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# encoding: utf-8

'''PDS Roundup: Node.js context'''

from .context import Context
from .errors import RoundupError, InvokedProcessError
from .step import Step, StepName, NullStep, RequirementsStep, DocPublicationStep, BaseChangeLogStep
from .util import git_config, invoke, invokeGIT, TAG_RE, add_version_label_to_open_bugs, commit, delete_tags
import shutil, logging, os, json, re

_logger = logging.getLogger(__name__)


class NodeJSContext(Context):
'''A Node.js context supports Node.js software proejcts'''
def __init__(self, cwd, environ, args):
self.steps = {
StepName.artifactPublication: _ArtifactPublicationStep,
StepName.build: _BuildStep,
StepName.changeLog: ChangeLogStep,
StepName.cleanup: _CleanupStep,
StepName.docPublication: _DocPublicationStep,
StepName.docs: _DocsStep,
StepName.githubRelease: _GitHubReleaseStep,
StepName.integrationTest: _IntegrationTestStep,
StepName.null: NullStep,
StepName.preparation: _PreparationStep,
StepName.requirements: RequirementsStep,
StepName.unitTest: _UnitTestStep,
StepName.versionBump: _VersionBumpingStep,
StepName.versionCommit: _VersionCommittingStep,
}
super().__init__(cwd, environ, args)


class _NodeJSStep(Step):
'''A Node.js-specific step.'''

def read_package_metadata(self):
'''Read the package.json file and yield its contents as a dict.'''
_logger.debug('Getting package metdadata from package.json')
try:
with open('package.json', 'r') as package_json_io:
return json.load(package_json_io)
except IOError as ex:
_logger.exception('Failed to read package.json: %s', str(ex))
raise RoundupError(ex)

def write_version_number(self, version_number):
'''Write the new ``version_number`` to the ``package.json`` file.'''
_logger.debug('Finding version in package.json')
try:
metadata = self.read_package_metadata()
metadata['version'] = version_number
with open('package.json', 'w') as package_json_io:
_logger.debug('Writing package.json with new version %s', version_number)
json.dump(metadata, package_json_io, sort_keys=True)
except IOError as ex:
_logger.exception('Failed to update package.json with new version %s: %s', version_number, str(ex))
raise RoundupError(ex)


class _PreparationStep(_NodeJSStep):
'''Prepare the python repository for action.'''
def execute(self):
_logger.debug('Node.js preparation step')
git_config()

# In github-actions-base, Node.js executables get installed in ~root/node_modules/.bin,
# so make sure that's on our PATH
os.environ['PATH'] = f'/root/node_modules/.bin:{os.environ["PATH"]}'

# Clean up any node_modules from earlier—if any
shutil.rmtree('node_modules', ignore_errors=True)

# Install our Node.js package
invoke(['npm', 'install'])

# ☑️ TODO: what other prep steps are there?


class _UnitTestStep(_NodeJSStep):
'''Unit test step, bruh.'''
def execute(self):
_logger.debug('Node.js unit test step')
invoke(['npm', 'test'])


class _IntegrationTestStep(_NodeJSStep):
'''A step to take for integration tests with Node.js; what actually happens here is yet
to be determined.
'''
def execute(self):
_logger.debug('Node.js integration test step; TBD')


class _DocsStep(_NodeJSStep):
'''A step that uses JSDoc (invoked by ``npm docs``) to generate documentation'''
def execute(self):
invoke(['npm', 'docs'])


class _VersionBumpingStep(_NodeJSStep):
'''Bump the version but do not commit it (yet).'''
def execute(self):
if not self.assembly.isStable():
_logger.debug('Skipping version bump for unstable build')
return

# Figure out the tag name; we use ``--tags`` to pick up all tags, not just the annotated
# ones. This'll help reduce erros by users who forget to annotate (``-a`` or ``--annoate``)
# their tags. The ``--abbrev 0`` truncates any post-tag commits
tag = invokeGIT(['describe', '--tags', '--abbrev=0', '--match', 'release/*']).strip()

if not tag:
raise RoundupError('🕊 Cannot determine the release tag; version bump failed')

match = TAG_RE.match(tag)
if not match:
raise RoundupError(f'🐎 Stable tag of «{tag}» but not a ``release/`` tag')

major, minor, micro = int(match.group(1)), int(match.group(2)), match.group(4)
full_version = f'{major}.{minor}.{micro}'
_logger.debug('🔖 So we got version %s', full_version)

if micro is None:
raise RoundupError('Invalid release version supplied in tag name. You must supply Major.Minor.Micro')

add_version_label_to_open_bugs(full_version)
self.write_version_number(full_version)


class _VersionCommittingStep(_NodeJSStep):
'''Commit the bumped version.'''
def execute(self):
if not self.assembly.isStable():
_logger.debug('Skipping version commit for unstable build')
return
commit('package.json', 'Commiting package.json for stable release')


class _BuildStep(_NodeJSStep):
'''A step that makes an installable package.'''
def execute(self):
if self.assembly.isStable():
# The package.json should already have the "stable" version number from the _VersionBumpingStep
# so we can just invoke ``npm build``
invoke(['npm', 'build'])
else:
# EA says we should rewrite the version number with ``-unstable`` in it
metadata = self.read_package_metadata()
unstable_version = metadata['version'] + '-unstable'
self.write_version_number(unstable_version)
invoke(['npm', 'build'])


class _GitHubReleaseStep(_NodeJSStep):
'''A step that releases software to GitHub
'''
def _pruneDev(self):
'''Get rid of any "dev" tags. Apparently we want to do this always; see
https://github.com/NASA-PDS/roundup-action/issues/32#issuecomment-776309904
'''
delete_tags('*dev*')

def _pruneReleaseTags(self):
'''Get rid of ``release/*`` tags.'''
delete_tags('release/*')

def _tagRelease(self):
'''Tag the current release using the v1.2.3-style tag based on the release/1.2.3-style tag.'''
_logger.debug('🏷 Tagging the release')
tag = invokeGIT(['describe', '--tags', '--abbrev=0', '--match', 'release/*']).strip()
if not tag:
_logger.debug('🕊 Cannot determine what tag we are currently on, so skipping re-tagging')
return
match = TAG_RE.match(tag)
if not match:
_logger.debug('🐎 Stable tag at «%s» but not a ``release/`` style tag, so skipping tagging', tag)
return
major, minor, micro = int(match.group(1)), int(match.group(2)), match.group(4)
_logger.debug('🔖 So we got version %d.%d.%s', major, minor, micro)
# roundup-action#90: we no longer bump the version number; just re-tag at the current HEAD
tag = f'v{major}.{minor}.{micro}'
_logger.debug('🆕 New tag will be %s', tag)
invokeGIT(['tag', '--annotate', '--force', '--message', f'Tag release {tag}', tag])
invokeGIT(['push', '--tags'])

def execute(self):
'''Execute the Node.js GitHub release step'''
_logger.debug('👣 Node.js GitHub release step')
token = self.getToken()
if not token:
_logger.info('🤷‍♀️ No GitHub administrative token; cannot release to GitHub')
return
self._pruneDev()
if self.assembly.isStable():
self._tagRelease()
invoke(['/usr/local/bin/node-release', '--debug', '--token', token])
else: # It's unstable release
invoke(['/usr/local/bin/node-release', '--debug', '--snapshot', '--token', token])
self._pruneReleaseTags()


class _ArtifactPublicationStep(_NodeJSStep):
'''A step that publishes artifacts to the npmjs.com'''
def execute(self):
try:
if self.assembly.isStable():
argv = ['npm', 'publish']
else:
argv = ['npm', 'publish', '--tag', 'unstable']
invoke(argv)
except InvokedProcessError:
# For unstalbe releases, we ignore this
if self.assembly.isStable(): raise


class _DocPublicationStep(DocPublicationStep):

default_documentation_dir = 'out'


class _CleanupStep(_NodeJSStep):
'''Step that tidies up.'
At this point we're cleaing up so errors are not longer considered awful.
'''
def execute(self):
_logger.debug('Node.js cleanup step')
if not self.assembly.isStable():
_logger.debug('Skipping cleanup for unstable build')
return

# NASA-PDS/roundup-action#99: delete the release/X.Y.Z tag
tag = invokeGIT(['describe', '--tags', '--abbrev=0', '--match', 'release/*']).strip()
if not tag:
raise RoundupError('🏷 Cannot determine the release tag at cleanup step')
invokeGIT(['push', 'origin', f':{tag}'])

version = self.read_package_metadata()['version']
match = re.match(r'(\d+)\.(\d+)\.(\d+)', version)
if not match:
_logger.info(f'Expected Major.Minor.Micro version in package.json but got «{version}» but whatever')
return
# NASA-PDS/roundup-action#81: Jordan would prefer the ``minor`` version get bumped, not the ``micro`` version:
major, minor, micro = int(match.group(1)), int(match.group(2)) + 1, int(match.group(3))
new_version = f'{major}.{minor}.0'
_logger.debug('🔖 Setting version %s in package.json', new_version)
self.write_version_number(new_version)
commit('package.json', f'Setting next dev version to {major}.{minor}.{micro}')


class ChangeLogStep(BaseChangeLogStep):
def execute(self):
_logger.debug('Node.js changelog step')
delete_tags('*dev*')
super().execute()
11 changes: 7 additions & 4 deletions src/pds/roundup/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,14 @@ def contextFactories():
# of a plain Python context.
from ._python import PythonContext
from ._maven import MavenContext
from ._nodejs import NodeJSContext
return {
'setup.cfg': PythonContext,
'setup.py': PythonContext,
'pom.xml': MavenContext,
'project.xml': MavenContext
'setup.cfg': PythonContext,
'setup.py': PythonContext,
'pom.xml': MavenContext,
'project.xml': MavenContext,
'package.json': NodeJSContext,
'package-lock.json': NodeJSContext
}


Expand Down

0 comments on commit 7ecd0a4

Please sign in to comment.