Copied to Clipboard
gitlab-yaml-sort.yml')
attributes = []
with open(sort_file_path, 'r') as f:
lines = f.readlines()
# Find the job section and extract attribute names
in_job_section = False
for line in lines:
line = line.strip()
if line.startswith('job:'):
in_job_section = True
continue
elif in_job_section and line and not line.startswith('#'):
# Extract attribute name (everything before the colon)
if ':' in line:
attr_name = line.split(':')[0].strip()
if attr_name:
attributes.append(attr_name)
return attributes
# Define the order in which job_attributes should be sorted
ATTRIBUTES_ORDER = get_attributes_order()
# Define special keywords to skip sorting of the following block. 'default' is a special keyword but should be sorted
IGNORED_TOP_LEVEL_KEYWORDS = ["stages", "includes", "variables", "workflow"]
# List of directories to ignore
IGNORED_DIRECTORIES = [".git", ".history", "node_modules", "tmp", ".gitlab-ci-local", "build-docs"]
# Define regex for matching filenames
GITLAB_FILENAME_REGEX = re.compile(r'.*\.gitlab-ci.*\.ya?ml$')
def sort_job_attributes(job_attributes):
sorted_job_attributes = sorted(job_attributes, key=lambda b: ATTRIBUTES_ORDER.index(b[0].lstrip().split(':')[0])
if b[0].lstrip().split(':')[0] in ATTRIBUTES_ORDER
else len(ATTRIBUTES_ORDER))
return [line for block in sorted_job_attributes for line in block]
def process_file(filename):
# make a backup of the original file in case of error processing it
shutil.copyfile(filename, filename + ".bak")
# Initialize variables for tracking job_attributes and lines
job_attributes = []
current_attribute = []
is_current_block_sortable = True
last_line_was_empty = False
with open(filename, 'r') as f:
lines = f.readlines()
with open(filename, 'w') as f:
for line in lines:
# Check if the line starts with special keywords
if any(line.startswith(keyword) for keyword in IGNORED_TOP_LEVEL_KEYWORDS):
# flush current attribute content
f.write(''.join(sort_job_attributes(job_attributes)))
job_attributes = []
current_attribute = []
if last_line_was_empty:
f.write('\n')
# don't reorganize and write
f.write(line)
is_current_block_sortable = False
# Check if the line is indented in a non-sortable block
elif line.startswith('' * 2) and not is_current_block_sortable:
# Just write
f.write(line)
# Check if the line is indented to sub-sublevel
elif line.startswith('' * 4):
# Add the line to the current block
current_attribute.append(line)
# Check if the line is indented to sublevel : this is the beginning of a new block to be sorted
elif line.startswith('' * 2):
# Add the current block to the list of job_attributes (if it's not empty)
current_attribute = [line]
job_attributes.append(current_attribute)
is_current_block_sortable = True
# Handle special case when there are empty lines in attributes (such as 'script')
elif line.strip() == '':
last_line_was_empty = True
# Otherwise, the line is not indented and should be written
else:
f.write(''.join(sort_job_attributes(job_attributes)))
if last_line_was_empty:
f.write('\n')
f.write(line)
# Reset variables and continue to the next line
job_attributes = []
current_attribute = []
is_current_block_sortable = True
last_line_was_empty = False
if current_attribute:
f.write(''.join(sort_job_attributes(job_attributes)))
print("successfully sorted job attributes in " + filename)
os.remove(filename + ".bak")
def process_files_recursively(directory):
for root, dirs, files in os.walk(directory):
# Remove ignored directories from the list
dirs[:] = [d for d in dirs if d not in IGNORED_DIRECTORIES]
for file in files:
if GITLAB_FILENAME_REGEX.match(file):
file_path = os.path.join(root, file)
process_file(file_path)
if __name__ == '__main__':
if len(sys.argv) == 1:
process_file(".gitlab-ci.yml")
elif len(sys.argv) == 2:
path = sys.argv[1]
if os.path.isfile(path):
process_file(path)
elif os.path.isdir(path):
process_files_recursively(path)
else:
print("usage: python {} [file|folder]".format(sys.argv[0]))
Wrapping up
Consistent attribute ordering in GitLab CI files is a small investment with significant returns: cleaner diffs, fewer merge conflicts, and improved readability. The gitlab-yaml-sort.py script automates this process while respecting the developer's formatting choices.
The key design decisions that make this tool effective:
-
Text-based processing preserves comments, blank lines, and formatting
-
External configuration allows easy customization without code changes
-
Logical attribute grouping creates a natural reading flow from inheritance to execution to conditions
Consider integrating this script into your pre-commit hooks or CI pipeline to enforce consistent styling across your team.
orange fox staring at a tall shelf in a library, a pile of books in the hands, anime style
Illustrations generated locally by Draw Things using Flux.1 [Schnell] model
Further reading
This article was enhanced with the assistance of an AI language model to ensure clarity and accuracy in the content, as English is not my native language.