diff --git a/.gitea/workflows/mvs_job.yaml b/.gitea/workflows/mvs_job.yaml index 94c4c21..447155e 100644 --- a/.gitea/workflows/mvs_job.yaml +++ b/.gitea/workflows/mvs_job.yaml @@ -31,7 +31,7 @@ jobs: nc -h echo "=== Debug: Setup complete ===" - - name: Find changed source files + - name: Find changed and deleted source files id: files run: | echo "=== Debug: Starting file detection ===" @@ -41,13 +41,16 @@ jobs: if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then echo "Parent commit exists; running git diff." CHANGED_FILES=$(git diff --name-only HEAD~1 2>/dev/null | grep -E '\.(c|bas)$' | head -1) + DELETED_FILES=$(git diff --name-only --diff-filter=D HEAD~1 2>/dev/null | grep -E '\.(c|bas)$') echo "Changed files from last commit: '${CHANGED_FILES}'" + echo "Deleted files from last commit: '${DELETED_FILES}'" else echo "No parent commit; skipping diff." CHANGED_FILES="" + DELETED_FILES="" fi echo "=== Debug: Git diff check complete ===" - + # Fallback to all .c/.bas files if no changes or no previous commit if [ -z "$CHANGED_FILES" ]; then echo "=== Debug: No changes found; running fallback find ===" @@ -56,22 +59,49 @@ jobs: echo "Fallback files (newest first): '${CHANGED_FILES}'" echo "=== Debug: Fallback complete ===" fi - - if [ -z "$CHANGED_FILES" ]; then + + if [ -z "$CHANGED_FILES" ] && [ -z "$DELETED_FILES" ]; then echo "No C/BAS files found; skipping workflow." exit 0 # Graceful skip, no failure fi - + echo "=== Debug: Processing final file ===" echo "Final selected file: '${CHANGED_FILES}'" echo "file=$CHANGED_FILES" >> $GITHUB_OUTPUT - + # Extract member name (handle .c or .bas) - EXT="${CHANGED_FILES##*.}" - BASE=$(basename "$CHANGED_FILES" ".$EXT") - echo "member=$BASE" >> $GITHUB_OUTPUT + if [ -n "$CHANGED_FILES" ]; then + EXT="${CHANGED_FILES##*.}" + BASE=$(basename "$CHANGED_FILES" ".$EXT") + echo "member=$BASE" >> $GITHUB_OUTPUT + fi + + # Process deleted files - convert to space-separated list of members + if [ -n "$DELETED_FILES" ]; then + DELETED_MEMBERS="" + for DFILE in $DELETED_FILES; do + DEXT="${DFILE##*.}" + DBASE=$(basename "$DFILE" ".$DEXT") + DELETED_MEMBERS="$DELETED_MEMBERS $DBASE" + done + echo "deleted_members=$DELETED_MEMBERS" >> $GITHUB_OUTPUT + fi echo "=== Debug: File detection complete ===" + - name: Delete removed members from PDS + if: ${{ steps.files.outputs.deleted_members != '' }} + run: | + echo "=== Debug: Starting deletion of removed members ===" + echo "Deleted members: ${{ steps.files.outputs.deleted_members }}" + for MEMBER in ${{ steps.files.outputs.deleted_members }}; do + echo "Deleting member: $MEMBER" + python3 delete_mvs_member.py "@05054.SRCLIB.C($MEMBER)" + done + echo "=== Debug: Deletion complete ===" + env: + MVS_BATCH_PASSWORD: ${{ vars.MVS_BATCH_PASSWORD }} + MVS_HOST: "oldcomputernerd.com" + - name: Upload to PDS and Submit JCL if: ${{ steps.files.outputs.file != '' }} run: | diff --git a/delete_mvs_member.py b/delete_mvs_member.py new file mode 100644 index 0000000..898f9fd --- /dev/null +++ b/delete_mvs_member.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +import sys +import subprocess +import tempfile +import os + +# Force temp files into a folder inside your project +custom_temp_dir = os.path.join(os.getcwd(), "tmp") +os.makedirs(custom_temp_dir, exist_ok=True) +tempfile.tempdir = custom_temp_dir + +MVSHOST = "oldcomputernerd.com" +RDRPORT = 3505 +MVS_PASSWORD = os.environ.get("MVS_BATCH_PASSWORD") + + +def create_delete_jcl(dataset_name, member_name): + """Create JCL to delete a PDS member using IEHPROGM""" + + jcl = f""" +//DELETE JOB (ACCT),'DELETE', +// USER=@05054,PASSWORD={MVS_PASSWORD}, +// CLASS=A,MSGCLASS=H,NOTIFY=@05054 +//DELMEM EXEC PGM=IEHPROGM +//SYSPRINT DD SYSOUT=* +//DD1 DD DSN={dataset_name},DISP=SHR +//SYSIN DD * + DELETE DSNAME={dataset_name},MEMBER={member_name} +/* +""" + return jcl + + +def delete_member(dataset_name, member_name, mvshost=MVSHOST): + """Delete a member from MVS PDS""" + + payload = create_delete_jcl(dataset_name, member_name) + + # Write JCL to temporary file and submit via netcat + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.jcl') as tmpfile: + tmpfile.write(payload) + tmpfile.flush() + tmpfile_path = tmpfile.name + + try: + with open(tmpfile_path, 'rb') as f: + result = subprocess.run( + ['nc', '-w', '5', mvshost, str(RDRPORT)], + input=f.read(), + check=True, + capture_output=True + ) + print(f"Deleted {dataset_name}({member_name})") + if result.stdout: + print("JES response:", result.stdout.decode(errors='ignore').strip()) + return 0 + except subprocess.CalledProcessError as e: + print(f"Deletion failed: {e}") + print("stderr:", e.stderr.decode(errors='ignore')) + return 1 + finally: + os.unlink(tmpfile_path) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: delete_mvs_member.py [mvshost]") + print() + print("Arguments:") + print(" pds_destination - PDS destination as DATASET(MEMBER) (required)") + print(" mvshost - MVS host (optional, default: oldcomputernerd.com)") + print() + print("Examples:") + print(" delete_mvs_member.py '@05054.SRCLIB.C(SIEVE11)'") + print(" delete_mvs_member.py '@05054.SRCLIB.C(HELLO)' mainframe.example.com") + sys.exit(1) + + destination = sys.argv[1] + + # Parse PDS syntax: DATASET(MEMBER) + if '(' in destination and destination.endswith(')'): + dataset_name = destination[:destination.index('(')] + member_name = destination[destination.index('(')+1:-1] + else: + print(f"Error: Invalid PDS syntax '{destination}'. Use format: DATASET(MEMBER)") + sys.exit(1) + + # Optional host override + mvshost = sys.argv[2] if len(sys.argv) > 2 else MVSHOST + + print(f"Deleting: {dataset_name}({member_name})") + print(f"Host: {mvshost}") + print() + + sys.exit(delete_member(dataset_name, member_name, mvshost))