Add deletion capabilities, reorganize, and update readme #4

Merged
gmgauthier merged 4 commits from delete_pds_member into master 2026-02-06 15:46:42 +00:00
6 changed files with 347 additions and 13 deletions

View File

@ -0,0 +1,83 @@
name: MVS Delete Members
on:
push:
branches: [ master ]
paths:
- 'src/**'
- 'jcl/**'
workflow_dispatch: # Allow manual trigger for cleanup
jobs:
delete-members:
runs-on: ubuntu-gitea
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for git diff
- name: Prepare environment
id: setup
run: |
echo "=== Debug: Starting setup ==="
apt-get update && apt install -y netcat-traditional python3-requests
nc -h
echo "=== Debug: Setup complete ==="
- name: Find deleted source files
id: deleted
run: |
echo "=== Debug: Starting deletion detection ==="
echo "Current dir: $(pwd)"
# Check if we have a parent commit
if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
echo "Parent commit exists; checking for deletions."
DELETED_FILES=$(git diff --name-only --diff-filter=D HEAD~1 2>/dev/null | grep -E '\.(c|bas)$')
echo "Deleted files from last commit: '${DELETED_FILES}'"
else
echo "No parent commit; no deletions to process."
DELETED_FILES=""
fi
if [ -z "$DELETED_FILES" ]; then
echo "No deleted C/BAS files found; skipping workflow."
echo "has_deletions=false" >> $GITHUB_OUTPUT
exit 0
fi
# Process deleted files - convert to space-separated list of members
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"
echo "deleted_members=$DELETED_MEMBERS" >> $GITHUB_OUTPUT
echo "has_deletions=true" >> $GITHUB_OUTPUT
echo "=== Debug: Deletion detection complete ==="
- name: Delete removed members from PDS
if: ${{ steps.deleted.outputs.has_deletions == 'true' }}
run: |
echo "=== Starting deletion of removed members ==="
echo "Deleted members: ${{ steps.deleted.outputs.deleted_members }}"
for MEMBER in ${{ steps.deleted.outputs.deleted_members }}; do
echo "Deleting member: $MEMBER"
python3 scripts/del_member.py "@05054.SRCLIB.C($MEMBER)"
done
echo "=== Deletion complete ==="
env:
MVS_BATCH_PASSWORD: ${{ vars.MVS_BATCH_PASSWORD }}
MVS_HOST: "oldcomputernerd.com"
- name: Report Status
if: ${{ steps.deleted.outputs.has_deletions == 'true' }}
run: |
echo "Deletion complete! Members removed from mainframe PDS."

View File

@ -1,4 +1,4 @@
name: MVS Upload & Execute
name: MVS Submit & Execute
on:
push:
@ -78,7 +78,7 @@ jobs:
echo "=== Debug: Starting upload/submit ==="
echo "File: ${{ steps.files.outputs.file }}"
echo "Member: ${{ steps.files.outputs.member }}"
python3 mvs_job.py "${{ steps.files.outputs.file }}" "@05054.SRCLIB.C(${{ steps.files.outputs.member }})"
python3 scripts/submit_job.py "${{ steps.files.outputs.file }}" "@05054.SRCLIB.C(${{ steps.files.outputs.member }})"
echo "=== Debug: Upload/submit complete ==="
env:
MVS_BATCH_PASSWORD: ${{ vars.MVS_BATCH_PASSWORD }}
@ -88,7 +88,7 @@ jobs:
if: ${{ steps.files.outputs.file != '' }}
run: |
echo "=== Waiting for job completion ==="
python3 poll_job.py "${{ steps.files.outputs.member }}" 120
python3 scripts/poll_job.py "${{ steps.files.outputs.member }}" 120
echo "=== Job output retrieved ==="
env:
MVS_CONSOLE_URL: ${{ vars.MVS_CONSOLE_URL }}

165
README.MD
View File

@ -1,10 +1,165 @@
# TK5 C90 Projects
This is an attempt to use the local GCC compiler and modern development tools,
to learn C90 (well, C89) on the MVS 3.8j Turnkey system.
A modern development workflow for C90 programming on IBM MVS 3.8j TK5, combining contemporary tooling with mainframe execution.
Essentially, you work from your IDE as you normally would. However, when you push to
the repository, the build is done on the the TK5 system by sending the source
file and the job execution JCL to the card reader port on the TK5.
## Overview
This project enables C90 (ANSI C89) development for MVS 3.8j using modern IDEs and Git workflows. Source code is written locally, then automatically uploaded and compiled on the mainframe via CI/CD automation.
## How It Works
1. **Local Development**: Write C90 code in your IDE with modern editing features
2. **Git Push**: Commit and push changes to the repository
3. **Automated Workflow**: Gitea Actions triggers mainframe operations:
- Uploads source files to PDS members via netcat to the JES card reader
- Submits JCL jobs for compilation and execution
- Polls for job completion and retrieves output
- Archives job output as build artifacts (PDFs)
4. **Sync Management**: Deleted files are automatically removed from the mainframe PDS
## Architecture
### Directory Structure
```
.
├── .gitea/workflows/ # CI/CD workflow definitions
│ ├── mvs_submit.yaml # Upload and execute workflow
│ └── mvs_delete.yaml # PDS member deletion workflow
├── scripts/ # Python automation scripts
│ ├── submit_job.py # Upload source & submit JCL
│ ├── del_member.py # Delete PDS members
│ └── poll_job.py # Poll for job completion
├── src/ # C90 source files
├── jcl/ # JCL job control files
└── tmp/ # Temporary files for workflow execution
```
### Workflows
**MVS Submit & Execute** (`mvs_submit.yaml`)
- **Triggers**: Push to master with changes in `src/` or `jcl/`
- **Actions**:
- Detects changed `.c` or `.bas` files
- Uploads source to `@05054.SRCLIB.C(MEMBER)`
- Submits corresponding JCL from `jcl/` directory
- Polls for job completion (120s timeout)
- Downloads and archives job output PDF
**MVS Delete Members** (`mvs_delete.yaml`)
- **Triggers**: Push to master with deletions in `src/` or `jcl/`, or manual dispatch
- **Actions**:
- Detects deleted `.c` or `.bas` files
- Submits IEHPROGM jobs to delete corresponding PDS members
- Keeps mainframe PDS in sync with repository
### Scripts
**submit_job.py**
- Uploads source files to MVS PDS using IEBUPDTE
- Submits JCL jobs via netcat to port 3505 (JES card reader)
- Handles PDS member creation and updates
**del_member.py**
- Deletes PDS members using IEHPROGM utility
- Maintains sync between repository and mainframe storage
**poll_job.py**
- Monitors job completion via MVS console
- Retrieves job output from remote printout directory
- Downloads PDF output for archival
## Requirements
### Mainframe
- MVS 3.8j TK5 system accessible via network
- JES card reader port (default: 3505)
- User account with appropriate permissions
### CI/CD Environment
- Gitea with Actions enabled
- Ubuntu runner with:
- `netcat-traditional`
- Python 3 with `requests` library
### Configuration Variables
Set in Gitea repository settings:
**Variables:**
- `MVS_BATCH_PASSWORD`: Password for batch job submission
- `MVS_CONSOLE_URL`: URL for MVS console access
- `MVS_CONSOLE_USER`: Console username
- `LINODE_SSH_HOST`: SSH host for printout retrieval
- `LINODE_PRINTOUT_DIR`: Remote directory for job output
**Secrets:**
- `MVS_CONSOLE_PASSWORD`: Console password
## Usage
### Adding New Programs
1. Create source file in `src/` (e.g., `HELLO.c`)
2. Create corresponding JCL in `jcl/` (e.g., `HELLO.jcl`)
3. Commit and push:
```bash
git add src/HELLO.c jcl/HELLO.jcl
git commit -m "Add HELLO program"
git push
```
4. Workflow automatically uploads and executes
5. Download job output from build artifacts
### Removing Programs
Simply delete the source file and push:
```bash
git rm src/OLDPROG.c
git commit -m "Remove OLDPROG"
git push
```
The deletion workflow automatically removes `@05054.SRCLIB.C(OLDPROG)` from the mainframe.
### Manual Cleanup
Trigger the deletion workflow manually via Gitea UI to clean up orphaned PDS members.
## Technical Details
### Upload Process
Source files are uploaded using JCL with IEBUPDTE:
1. Python script generates upload JCL dynamically
2. JCL includes `./ ADD NAME=MEMBER` control statements
3. Source lines are padded to 80 characters (LRECL=80)
4. Comment lines starting with `//` or `/*` are stripped to avoid JCL conflicts
5. Submitted via netcat to JES internal reader
### Deletion Process
PDS members are deleted using IEHPROGM:
```jcl
//DELMEM EXEC PGM=IEHPROGM
//DD1 DD DSN=dataset,DISP=SHR
//SYSIN DD *
DELETE DSNAME=dataset,MEMBER=member
```
### Job Monitoring
Polls MVS console for job completion, then retrieves PDF output via SSH from remote printout directory.
## Development Notes
This project serves as a learning environment for:
- C90/ANSI C89 programming
- MVS JCL and utilities (IEBUPDTE, IEHPROGM)
- Mainframe batch processing workflows
- CI/CD integration with legacy systems
- Network protocols for mainframe communication
## License
Educational/personal use project.

96
scripts/del_member.py Normal file
View File

@ -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 <pds_destination> [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))