Adding awx_ as well as tower_ variable names for webhooks (#11925)

Adding utility to ease testing webhooks from command line
Modifying all variables to use a constants list of variable names
This commit is contained in:
John Westcott IV
2022-03-24 11:58:15 -04:00
committed by GitHub
parent fcdff8bdfb
commit 593eebf062
11 changed files with 438 additions and 96 deletions

View File

@@ -16,7 +16,7 @@ from awx.api import serializers
from awx.api.generics import APIView, GenericAPIView from awx.api.generics import APIView, GenericAPIView
from awx.api.permissions import WebhookKeyPermission from awx.api.permissions import WebhookKeyPermission
from awx.main.models import Job, JobTemplate, WorkflowJob, WorkflowJobTemplate from awx.main.models import Job, JobTemplate, WorkflowJob, WorkflowJobTemplate
from awx.main.constants import JOB_VARIABLE_PREFIXES
logger = logging.getLogger('awx.api.views.webhooks') logger = logging.getLogger('awx.api.views.webhooks')
@@ -136,15 +136,16 @@ class WebhookReceiverBase(APIView):
'webhook_credential': obj.webhook_credential, 'webhook_credential': obj.webhook_credential,
'webhook_guid': event_guid, 'webhook_guid': event_guid,
}, },
'extra_vars': { 'extra_vars': {},
'tower_webhook_event_type': event_type,
'tower_webhook_event_guid': event_guid,
'tower_webhook_event_ref': event_ref,
'tower_webhook_status_api': status_api,
'tower_webhook_payload': request.data,
},
} }
for name in JOB_VARIABLE_PREFIXES:
kwargs['extra_vars']['{}_webhook_event_type'.format(name)] = event_type
kwargs['extra_vars']['{}_webhook_event_guid'.format(name)] = event_guid
kwargs['extra_vars']['{}_webhook_event_ref'.format(name)] = event_ref
kwargs['extra_vars']['{}_webhook_status_api'.format(name)] = status_api
kwargs['extra_vars']['{}_webhook_payload'.format(name)] = request.data
new_job = obj.create_unified_job(**kwargs) new_job = obj.create_unified_job(**kwargs)
new_job.signal_start() new_job.signal_start()

View File

@@ -95,3 +95,8 @@ CONTAINER_VOLUMES_MOUNT_TYPES = ['z', 'O', 'ro', 'rw']
MAX_ISOLATED_PATH_COLON_DELIMITER = 2 MAX_ISOLATED_PATH_COLON_DELIMITER = 2
SURVEY_TYPE_MAPPING = {'text': str, 'textarea': str, 'password': str, 'multiplechoice': str, 'multiselect': str, 'integer': int, 'float': (float, int)} SURVEY_TYPE_MAPPING = {'text': str, 'textarea': str, 'password': str, 'multiplechoice': str, 'multiselect': str, 'integer': int, 'float': (float, int)}
JOB_VARIABLE_PREFIXES = [
'awx',
'tower',
]

View File

@@ -55,6 +55,7 @@ from awx.main.models.mixins import (
WebhookMixin, WebhookMixin,
WebhookTemplateMixin, WebhookTemplateMixin,
) )
from awx.main.constants import JOB_VARIABLE_PREFIXES
logger = logging.getLogger('awx.main.models.jobs') logger = logging.getLogger('awx.main.models.jobs')
@@ -770,14 +771,14 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
def awx_meta_vars(self): def awx_meta_vars(self):
r = super(Job, self).awx_meta_vars() r = super(Job, self).awx_meta_vars()
if self.project: if self.project:
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_project_revision'.format(name)] = self.project.scm_revision r['{}_project_revision'.format(name)] = self.project.scm_revision
r['{}_project_scm_branch'.format(name)] = self.project.scm_branch r['{}_project_scm_branch'.format(name)] = self.project.scm_branch
if self.scm_branch: if self.scm_branch:
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_job_scm_branch'.format(name)] = self.scm_branch r['{}_job_scm_branch'.format(name)] = self.scm_branch
if self.job_template: if self.job_template:
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_job_template_id'.format(name)] = self.job_template.pk r['{}_job_template_id'.format(name)] = self.job_template.pk
r['{}_job_template_name'.format(name)] = self.job_template.name r['{}_job_template_name'.format(name)] = self.job_template.name
return r return r

View File

@@ -583,7 +583,7 @@ class WebhookMixin(models.Model):
if not self.webhook_credential: if not self.webhook_credential:
return return
status_api = self.extra_vars_dict.get('tower_webhook_status_api') status_api = self.extra_vars_dict.get('awx_webhook_status_api')
if not status_api: if not status_api:
logger.debug("Webhook event did not have a status API endpoint associated, skipping.") logger.debug("Webhook event did not have a status API endpoint associated, skipping.")
return return

View File

@@ -51,7 +51,7 @@ from awx.main.utils.common import (
) )
from awx.main.utils.encryption import encrypt_dict, decrypt_field from awx.main.utils.encryption import encrypt_dict, decrypt_field
from awx.main.utils import polymorphic from awx.main.utils import polymorphic
from awx.main.constants import ACTIVE_STATES, CAN_CANCEL from awx.main.constants import ACTIVE_STATES, CAN_CANCEL, JOB_VARIABLE_PREFIXES
from awx.main.redact import UriCleaner, REPLACE_STR from awx.main.redact import UriCleaner, REPLACE_STR
from awx.main.consumers import emit_channel_notification from awx.main.consumers import emit_channel_notification
from awx.main.fields import AskForField, OrderedManyToManyField from awx.main.fields import AskForField, OrderedManyToManyField
@@ -1450,7 +1450,7 @@ class UnifiedJob(
by AWX, for purposes of client playbook hooks by AWX, for purposes of client playbook hooks
""" """
r = {} r = {}
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_job_id'.format(name)] = self.pk r['{}_job_id'.format(name)] = self.pk
r['{}_job_launch_type'.format(name)] = self.launch_type r['{}_job_launch_type'.format(name)] = self.launch_type
@@ -1459,7 +1459,7 @@ class UnifiedJob(
wj = self.get_workflow_job() wj = self.get_workflow_job()
if wj: if wj:
schedule = getattr_dne(wj, 'schedule') schedule = getattr_dne(wj, 'schedule')
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_workflow_job_id'.format(name)] = wj.pk r['{}_workflow_job_id'.format(name)] = wj.pk
r['{}_workflow_job_name'.format(name)] = wj.name r['{}_workflow_job_name'.format(name)] = wj.name
r['{}_workflow_job_launch_type'.format(name)] = wj.launch_type r['{}_workflow_job_launch_type'.format(name)] = wj.launch_type
@@ -1470,12 +1470,12 @@ class UnifiedJob(
if not created_by: if not created_by:
schedule = getattr_dne(self, 'schedule') schedule = getattr_dne(self, 'schedule')
if schedule: if schedule:
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_schedule_id'.format(name)] = schedule.pk r['{}_schedule_id'.format(name)] = schedule.pk
r['{}_schedule_name'.format(name)] = schedule.name r['{}_schedule_name'.format(name)] = schedule.name
if created_by: if created_by:
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_user_id'.format(name)] = created_by.pk r['{}_user_id'.format(name)] = created_by.pk
r['{}_user_name'.format(name)] = created_by.username r['{}_user_name'.format(name)] = created_by.username
r['{}_user_email'.format(name)] = created_by.email r['{}_user_email'.format(name)] = created_by.email
@@ -1484,7 +1484,7 @@ class UnifiedJob(
inventory = getattr_dne(self, 'inventory') inventory = getattr_dne(self, 'inventory')
if inventory: if inventory:
for name in ('awx', 'tower'): for name in JOB_VARIABLE_PREFIXES:
r['{}_inventory_id'.format(name)] = inventory.pk r['{}_inventory_id'.format(name)] = inventory.pk
r['{}_inventory_name'.format(name)] = inventory.name r['{}_inventory_name'.format(name)] = inventory.name

View File

@@ -10,6 +10,7 @@ from django.contrib.contenttypes.models import ContentType
# AWX # AWX
from awx.main.models import UnifiedJobTemplate, Job, JobTemplate, WorkflowJobTemplate, WorkflowApprovalTemplate, Project, WorkflowJob, Schedule, Credential from awx.main.models import UnifiedJobTemplate, Job, JobTemplate, WorkflowJobTemplate, WorkflowApprovalTemplate, Project, WorkflowJob, Schedule, Credential
from awx.api.versioning import reverse from awx.api.versioning import reverse
from awx.main.constants import JOB_VARIABLE_PREFIXES
@pytest.mark.django_db @pytest.mark.django_db
@@ -125,17 +126,19 @@ class TestMetaVars:
workflow_job.workflow_nodes.create(job=job) workflow_job.workflow_nodes.create(job=job)
data = job.awx_meta_vars() data = job.awx_meta_vars()
assert data['awx_user_id'] == admin_user.id for name in JOB_VARIABLE_PREFIXES:
assert data['awx_user_name'] == admin_user.username assert data['{}_user_id'.format(name)] == admin_user.id
assert data['awx_workflow_job_id'] == workflow_job.pk assert data['{}_user_name'.format(name)] == admin_user.username
assert data['awx_workflow_job_launch_type'] == workflow_job.launch_type assert data['{}_workflow_job_id'.format(name)] == workflow_job.pk
assert data['{}_workflow_job_launch_type'.format(name)] == workflow_job.launch_type
def test_scheduled_job_metavars(self, job_template, admin_user): def test_scheduled_job_metavars(self, job_template, admin_user):
schedule = Schedule.objects.create(name='job-schedule', rrule='DTSTART:20171129T155939z\nFREQ=MONTHLY', unified_job_template=job_template) schedule = Schedule.objects.create(name='job-schedule', rrule='DTSTART:20171129T155939z\nFREQ=MONTHLY', unified_job_template=job_template)
job = Job.objects.create(name='fake-job', launch_type='workflow', schedule=schedule, job_template=job_template) job = Job.objects.create(name='fake-job', launch_type='workflow', schedule=schedule, job_template=job_template)
data = job.awx_meta_vars() data = job.awx_meta_vars()
assert data['awx_schedule_id'] == schedule.pk for name in JOB_VARIABLE_PREFIXES:
assert 'awx_user_name' not in data assert data['{}_schedule_id'.format(name)] == schedule.pk
assert '{}_user_name'.format(name) not in data
def test_scheduled_workflow_job_node_metavars(self, workflow_job_template): def test_scheduled_workflow_job_node_metavars(self, workflow_job_template):
schedule = Schedule.objects.create(name='job-schedule', rrule='DTSTART:20171129T155939z\nFREQ=MONTHLY', unified_job_template=workflow_job_template) schedule = Schedule.objects.create(name='job-schedule', rrule='DTSTART:20171129T155939z\nFREQ=MONTHLY', unified_job_template=workflow_job_template)
@@ -144,22 +147,16 @@ class TestMetaVars:
job = Job.objects.create(launch_type='workflow') job = Job.objects.create(launch_type='workflow')
workflow_job.workflow_nodes.create(job=job) workflow_job.workflow_nodes.create(job=job)
assert job.awx_meta_vars() == { result_hash = {}
'awx_job_id': job.id, for name in JOB_VARIABLE_PREFIXES:
'tower_job_id': job.id, result_hash['{}_job_id'.format(name)] = job.id
'awx_job_launch_type': 'workflow', result_hash['{}_job_launch_type'.format(name)] = 'workflow'
'tower_job_launch_type': 'workflow', result_hash['{}_workflow_job_name'.format(name)] = 'workflow-job'
'awx_workflow_job_name': 'workflow-job', result_hash['{}_workflow_job_id'.format(name)] = workflow_job.id
'tower_workflow_job_name': 'workflow-job', result_hash['{}_workflow_job_launch_type'.format(name)] = workflow_job.launch_type
'awx_workflow_job_id': workflow_job.id, result_hash['{}_parent_job_schedule_id'.format(name)] = schedule.id
'tower_workflow_job_id': workflow_job.id, result_hash['{}_parent_job_schedule_name'.format(name)] = 'job-schedule'
'awx_workflow_job_launch_type': workflow_job.launch_type, assert job.awx_meta_vars() == result_hash
'tower_workflow_job_launch_type': workflow_job.launch_type,
'awx_parent_job_schedule_id': schedule.id,
'tower_parent_job_schedule_id': schedule.id,
'awx_parent_job_schedule_name': 'job-schedule',
'tower_parent_job_schedule_name': 'job-schedule',
}
@pytest.mark.django_db @pytest.mark.django_db

View File

@@ -2,6 +2,7 @@ import pytest
from unittest import mock from unittest import mock
from awx.main.models import UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowApprovalTemplate, Job, User, Project, JobTemplate, Inventory from awx.main.models import UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowApprovalTemplate, Job, User, Project, JobTemplate, Inventory
from awx.main.constants import JOB_VARIABLE_PREFIXES
def test_incorrectly_formatted_variables(): def test_incorrectly_formatted_variables():
@@ -83,26 +84,18 @@ class TestMetaVars:
def test_job_metavars(self): def test_job_metavars(self):
maker = User(username='joe', pk=47, id=47) maker = User(username='joe', pk=47, id=47)
inv = Inventory(name='example-inv', id=45) inv = Inventory(name='example-inv', id=45)
assert Job(name='fake-job', pk=42, id=42, launch_type='manual', created_by=maker, inventory=inv).awx_meta_vars() == { result_hash = {}
'tower_job_id': 42, for name in JOB_VARIABLE_PREFIXES:
'awx_job_id': 42, result_hash['{}_job_id'.format(name)] = 42
'tower_job_launch_type': 'manual', result_hash['{}_job_launch_type'.format(name)] = 'manual'
'awx_job_launch_type': 'manual', result_hash['{}_user_name'.format(name)] = 'joe'
'awx_user_name': 'joe', result_hash['{}_user_email'.format(name)] = ''
'tower_user_name': 'joe', result_hash['{}_user_first_name'.format(name)] = ''
'awx_user_email': '', result_hash['{}_user_last_name'.format(name)] = ''
'tower_user_email': '', result_hash['{}_user_id'.format(name)] = 47
'awx_user_first_name': '', result_hash['{}_inventory_id'.format(name)] = 45
'tower_user_first_name': '', result_hash['{}_inventory_name'.format(name)] = 'example-inv'
'awx_user_last_name': '', assert Job(name='fake-job', pk=42, id=42, launch_type='manual', created_by=maker, inventory=inv).awx_meta_vars() == result_hash
'tower_user_last_name': '',
'awx_user_id': 47,
'tower_user_id': 47,
'tower_inventory_id': 45,
'awx_inventory_id': 45,
'tower_inventory_name': 'example-inv',
'awx_inventory_name': 'example-inv',
}
def test_project_update_metavars(self): def test_project_update_metavars(self):
data = Job( data = Job(
@@ -113,7 +106,8 @@ class TestMetaVars:
project=Project(name='jobs-sync', scm_revision='12345444'), project=Project(name='jobs-sync', scm_revision='12345444'),
job_template=JobTemplate(name='jobs-jt', id=92, pk=92), job_template=JobTemplate(name='jobs-jt', id=92, pk=92),
).awx_meta_vars() ).awx_meta_vars()
assert data['awx_project_revision'] == '12345444' for name in JOB_VARIABLE_PREFIXES:
assert 'tower_job_template_id' in data assert data['{}_project_revision'.format(name)] == '12345444'
assert data['tower_job_template_id'] == 92 assert '{}_job_template_id'.format(name) in data
assert data['tower_job_template_name'] == 'jobs-jt' assert data['{}_job_template_id'.format(name)] == 92
assert data['{}_job_template_name'.format(name)] == 'jobs-jt'

View File

@@ -39,6 +39,7 @@ from awx.main.utils.safe_yaml import SafeLoader
from awx.main.utils.execution_environments import CONTAINER_ROOT, to_host_path from awx.main.utils.execution_environments import CONTAINER_ROOT, to_host_path
from awx.main.utils.licensing import Licenser from awx.main.utils.licensing import Licenser
from awx.main.constants import JOB_VARIABLE_PREFIXES
class TestJobExecution(object): class TestJobExecution(object):
@@ -363,32 +364,14 @@ class TestExtraVarSanitation(TestJobExecution):
extra_vars = yaml.load(fd, Loader=SafeLoader) extra_vars = yaml.load(fd, Loader=SafeLoader)
# ensure that strings are marked as unsafe # ensure that strings are marked as unsafe
for unsafe in [ for name in JOB_VARIABLE_PREFIXES:
'awx_job_template_name', for variable_name in ['_job_template_name', '_user_name', '_job_launch_type', '_project_revision', '_inventory_name']:
'tower_job_template_name', assert hasattr(extra_vars['{}{}'.format(name, variable_name)], '__UNSAFE__')
'awx_user_name',
'tower_job_launch_type',
'awx_project_revision',
'tower_project_revision',
'tower_user_name',
'awx_job_launch_type',
'awx_inventory_name',
'tower_inventory_name',
]:
assert hasattr(extra_vars[unsafe], '__UNSAFE__')
# ensure that non-strings are marked as safe # ensure that non-strings are marked as safe
for safe in [ for name in JOB_VARIABLE_PREFIXES:
'awx_job_template_id', for variable_name in ['_job_template_id', '_job_id', '_user_id', '_inventory_id']:
'awx_job_id', assert not hasattr(extra_vars['{}{}'.format(name, variable_name)], '__UNSAFE__')
'awx_user_id',
'tower_user_id',
'tower_job_template_id',
'tower_job_id',
'awx_inventory_id',
'tower_inventory_id',
]:
assert not hasattr(extra_vars[safe], '__UNSAFE__')
def test_launchtime_vars_unsafe(self, job, private_data_dir, mock_me): def test_launchtime_vars_unsafe(self, job, private_data_dir, mock_me):
job.extra_vars = json.dumps({'msg': self.UNSAFE}) job.extra_vars = json.dumps({'msg': self.UNSAFE})
@@ -552,10 +535,9 @@ class TestGenericRun:
call_args, _ = task._write_extra_vars_file.call_args_list[0] call_args, _ = task._write_extra_vars_file.call_args_list[0]
private_data_dir, extra_vars, safe_dict = call_args private_data_dir, extra_vars, safe_dict = call_args
assert extra_vars['tower_user_id'] == 123 for name in JOB_VARIABLE_PREFIXES:
assert extra_vars['tower_user_name'] == "angry-spud" assert extra_vars['{}_user_id'.format(name)] == 123
assert extra_vars['awx_user_id'] == 123 assert extra_vars['{}_user_name'.format(name)] == "angry-spud"
assert extra_vars['awx_user_name'] == "angry-spud"
def test_survey_extra_vars(self, mock_me): def test_survey_extra_vars(self, mock_me):
job = Job() job = Job()
@@ -640,10 +622,9 @@ class TestAdhocRun(TestJobExecution):
call_args, _ = task._write_extra_vars_file.call_args_list[0] call_args, _ = task._write_extra_vars_file.call_args_list[0]
private_data_dir, extra_vars = call_args private_data_dir, extra_vars = call_args
assert extra_vars['tower_user_id'] == 123 for name in JOB_VARIABLE_PREFIXES:
assert extra_vars['tower_user_name'] == "angry-spud" assert extra_vars['{}_user_id'.format(name)] == 123
assert extra_vars['awx_user_id'] == 123 assert extra_vars['{}_user_name'.format(name)] == "angry-spud"
assert extra_vars['awx_user_name'] == "angry-spud"
class TestJobCredentials(TestJobExecution): class TestJobCredentials(TestJobExecution):

136
tools/scripts/post_webhook.py Executable file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python
from hashlib import sha1
from sys import exit
import click
import hmac
import http.client as http_client
import json
import logging
import requests
import urllib3
import uuid
@click.command()
@click.option('--file', required=True, help='File containing the post data.')
@click.option('--key', "webhook_key", required=True, help='The webhook key for the job template.')
@click.option('--url', required=True, help='The webhook url for the job template (i.e. https://tower.jowestco.net:8043/api/v2/job_templates/637/github/.')
@click.option('--event-type', help='Specific value for Event header, defaults to "issues" for GitHub and "Push Hook" for GitLab')
@click.option('--verbose', is_flag=True, help='Dump HTTP communication for debugging')
@click.option('--insecure', is_flag=True, help='Ignore SSL certs if true')
def post_webhook(file, webhook_key, url, verbose, event_type, insecure):
"""
Helper command for submitting POST requests to Webhook endpoints.
We have two sample webhooks in tools/scripts/webhook_examples for gitlab and github.
These or any other file can be pointed to with the --file parameter.
\b
Additional example webhook events can be found online.
For GitLab see:
https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html
\b
For GitHub see:
https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
\b
For setting up webhooks in AWX see:
https://docs.ansible.com/ansible-tower/latest/html/userguide/webhooks.html
\b
Example usage for GitHub:
./post_webhook.py \\
--file webhook_examples/github_push.json \\
--url https://tower.jowestco.net:8043/api/v2/job_templates/637/github/ \\
--key AvqBR19JDFaLTsbF3p7FmiU9WpuHsJKdHDfTqKXyzv1HtwDGZ8 \\
--insecure \\
--type github
\b
Example usage for GitLab:
./post_webhook.py \\
--file webhook_examples/gitlab_push.json \\
--url https://tower.jowestco.net:8043/api/v2/job_templates/638/gitlab/ \\
--key fZ8vUpfHfb1Dn7zHtyaAsyZC5IHFcZf2a2xiBc2jmrBDptCOL2 \\
--insecure \\
--type=gitlab
\b
NOTE: GitLab webhooks are stored in the DB with a UID of the hash of the POST body.
After submitting one post GitLab post body a second POST of the same payload
can result in a response like:
Response code: 202
Response body:
{
"message": "Webhook previously received, aborting."
}
If you need to test multiple GitLab posts simply change your payload slightly
"""
if insecure:
# Disable insecure warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
if verbose:
# Enable HTTP debugging
http_client.HTTPConnection.debuglevel = 1
# Configure logging
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
# read webhook payload
with open(file, 'r') as f:
post_data = json.loads(f.read())
# Construct Headers
headers = {
'Content-Type': 'application/json',
}
# Encode key and post_data
key_bytes = webhook_key.encode('utf-8', 'strict')
data_bytes = str(json.dumps(post_data)).encode('utf-8', 'strict')
# Compute sha1 mac
mac = hmac.new(key_bytes, msg=data_bytes, digestmod=sha1)
if url.endswith('/github/'):
headers.update(
{
'X-Hub-Signature': 'sha1={}'.format(mac.hexdigest()),
'X-GitHub-Event': 'issues' if event_type == 'default' else event_type,
'X-GitHub-Delivery': str(uuid.uuid4()),
}
)
elif url.endswith('/gitlab/'):
mac = hmac.new(key_bytes, msg=data_bytes, digestmod=sha1)
headers.update(
{
'X-GitLab-Event': 'Push Hook' if event_type == 'default' else event_type,
'X-GitLab-Token': webhook_key,
}
)
else:
click.echo("This utility only knows how to support URLs that end in /github/ or /gitlab/.")
exit(250)
# Make post
r = requests.post(url, data=json.dumps(post_data), headers=headers, verify=(not insecure))
if not verbose:
click.echo("Response code: {}".format(r.status_code))
click.echo("Response body:")
try:
click.echo(json.dumps(r.json(), indent=4))
except:
click.echo(r.text)
if __name__ == '__main__':
post_webhook()

View File

@@ -0,0 +1,156 @@
{
"ref": "refs/tags/simple-tag",
"before": "0000000000000000000000000000000000000000",
"after": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"created": true,
"deleted": false,
"forced": false,
"base_ref": "refs/heads/main",
"compare": "https://github.com/Codertocat/Hello-World/compare/simple-tag",
"commits": [],
"head_commit": {
"id": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"tree_id": "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
"distinct": true,
"message": "Adding a .gitignore file",
"timestamp": "2019-05-15T15:20:41Z",
"url": "https://github.com/Codertocat/Hello-World/commit/6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"author": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com",
"username": "Codertocat"
},
"committer": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com",
"username": "Codertocat"
},
"added": [
".gitignore"
],
"removed": [],
"modified": []
},
"repository": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com",
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://github.com/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": 1557933565,
"updated_at": "2019-05-15T15:20:41Z",
"pushed_at": 1557933657,
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Ruby",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 1,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 1,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"stargazers": 0,
"master_branch": "master"
},
"pusher": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com"
},
"sender": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
}
}

View File

@@ -0,0 +1,71 @@
{
"object_kind": "push",
"event_name": "push",
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"ref": "refs/heads/master",
"checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"user_id": 4,
"user_name": "John Smith",
"user_username": "jsmith",
"user_email": "john@example.com",
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
"project":{
"id": 15,
"name":"Diaspora",
"description":"",
"web_url":"http://example.com/mike/diaspora",
"avatar_url":null,
"git_ssh_url":"git@example.com:mike/diaspora.git",
"git_http_url":"http://example.com/mike/diaspora.git",
"namespace":"Mike",
"visibility_level":0,
"path_with_namespace":"mike/diaspora",
"default_branch":"master",
"homepage":"http://example.com/mike/diaspora",
"url":"git@example.com:mike/diaspora.git",
"ssh_url":"git@example.com:mike/diaspora.git",
"http_url":"http://example.com/mike/diaspora.git"
},
"repository":{
"name": "Diaspora",
"url": "git@example.com:mike/diaspora.git",
"description": "",
"homepage": "http://example.com/mike/diaspora",
"git_http_url":"http://example.com/mike/diaspora.git",
"git_ssh_url":"git@example.com:mike/diaspora.git",
"visibility_level":0
},
"commits": [
{
"id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
"message": "Update Catalan translation to e38cb41.\n\nSee https://gitlab.com/gitlab-org/gitlab for more information",
"title": "Update Catalan translation to e38cb41.",
"timestamp": "2011-12-12T14:27:31+02:00",
"url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
"author": {
"name": "Jordi Mallach",
"email": "jordi@softcatala.org"
},
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
},
{
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"message": "fixed readme",
"title": "fixed readme",
"timestamp": "2012-01-03T23:36:29+02:00",
"url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"author": {
"name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)"
},
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
}
],
"total_commits_count": 4
}