Well, there's always DFSORT to the rescue:
//STEP0100 EXEC PGM=SORT
//SYSOUT DD SYSOUT=*
//SORTOUT DD SYSOUT=*
//SYSIN DD *
OPTION EQUALS
INREC IFTHEN=(WHEN=INIT,
OVERLAY=(81:C'257')),
IFTHEN=(WHEN=INIT,
PARSE=(%00=(STARTAFT=BLANKS,FIXLEN=5))),
IFTHEN=(WHEN=INIT,
OVERLAY=(84:%00)),
IFTHEN=(WHEN=GROUP,
BEGIN=(1,2,CH,EQ,C'//',
AND,3,1,CH,NE,C'*',
AND,84,4,CH,EQ,C'EXEC '),
PUSH=(81:ID=3))
SORT FIELDS=(81,3,CH,D)
OUTREC BUILD=(1,80)
//SORTIN DD DATA,DLM=ZZ
//TEXT JOB STUFF
// OTHER STUFF BEFORE FIRST STEP
// EXEC 1
//* EXEC PART OF STEP 1, BUT A COMMENT
// 1
// EXEC 2
// 2
// EXEC 3
// 3
ZZ
Produces this:
//TEXT JOB STUFF
// OTHER STUFF BEFORE FIRST STEP
// EXEC 3
// 3
// EXEC 2
// 2
// EXEC 1
//* EXEC PART OF STEP 1, BUT A COMMENT
// 1
The record is "extended" by adding the characters "257" at position 81. 257 is more steps than you can have in a job. PARSE is then used to find the first field after one or more blanks, and five bytes are extracted. This is appended after the 257. WHEN=GROUP is used, starting if a JCL line and not a "comment" and the second extension is "EXEC ", to identify the start of a step. A 3-byte ID is PUSHed, overwriting the "257". This ID will be on all records of the group.
The data is SORTed, descending, with OPTION EQUALS, on the "257"/ID field, leaving the "job-related" statements at the top, and then producing the steps in reverse order.
This would only be of any use if there is no dependency in the steps from one to another.