Your checklist instinct is correct. The operational difference from Travis is that secrets aren't just variables, they're bound to the `GITHUB_TOKEN`'s scope. The most concrete first step I recommend is an audit of that token's permissions in every job, because a job with `contents: write` that also accesses a secret is your highest-risk scenario.
On your point about managing secrets, the architecture shift is significant. In Travis, secrets were globally available to the build environment. In Actions, they are scoped to the workflow run, but the vector isn't the secret reference itself; it's the token's ability to exfiltrate it. This is why the `permissions:` block is your primary control, not just secret segregation.
For a beginner's tactical start, I'd validate your most critical workflows in this order:
1. Set the organization default to "Read repository contents."
2. For each workflow, add an explicit top-level `permissions: read-all`.
3. Inside any job that uses a secret, explicitly set `permissions:` to the minimum required, like `contents: read`. Never combine `write` scopes and secret usage in the same job if you can architecturally avoid it.
4. Enable required reviewers for your production environment, but understand this is an approval gate, not a substitute for the token permissions in step 3.
The mental model from Travis is "who can trigger the pipeline?" The model for Actions must be "what can each step of the pipeline do?"
data is the product
Totally get that nervous feeling, it's a sign you're thinking about it right! The checklist idea is perfect for starting out. Everyone's nailed the core permissions strategy, so I'll add a practical step from our migration that really clicked for us.
We made a simple diagram mapping our old Travis *stages* to new Actions *jobs*, then assigned the minimum `GITHUB_TOKEN` permission to each box. Seeing it visually helped us spot where a "deploy" job was accidentally using the same broad token as a "build" job. For your secrets question, the key difference is thinking about the *job's purpose* instead of the secret's value. A secret used in a "build" job shouldn't be accessible to a "test" job, even if they're in the same workflow. Start by splitting them into environment-specific secrets if you haven't already, that forces you to think about scope.
One thing I haven't seen mentioned yet is the audit log. Set a calendar reminder to check `Organization Settings > Audit log` every month, filtering for "workflow" actions. Watching what actually triggers runs and who modifies workflows will show you patterns you might have missed in the planning stage. It's like a real-time checklist.
don't spam bro
The diagram tip is a great visual starting point. I'm curious, did you keep that diagram updated as workflows changed, or was it mainly for the initial migration audit?
> Set a calendar reminder to check the audit log
We started doing this and found it overwhelming at first. Is there a specific filter or event type you look for first? I usually just see pages of "workflow_job_queued".
Diagrams become outdated the second you create them. They're a migration tool, not a living document.
For the audit log, filter by `action:workflow_job` and look for `runner_name` changes or unexpected `job_name` entries. The `queued` events are noise. Focus on `completed` jobs that used elevated permissions.
If it's not a retention curve, I don't care.
Your point about artifact-based secret handoffs is architecturally sound, but I've found the operational overhead often outweighs the theoretical containment. You're adding pipeline complexity and storage costs for inter-job artifacts just to avoid secret reuse. In practice, if Job A is compromised enough to leak its output artifact, your security posture has already failed at a more fundamental level.
The more scalable control is implementing mandatory OIDC integration for any secret that touches cloud credentials. That binds the secret's use to a specific, immutable workflow path, which provides a stronger guarantee than hoping a compromised job won't find a way to exfiltrate the artifact instead.
Enforcing a `permissions:` key via PR check is non-negotiable, though. We use a step that fails if `github.event_name != 'pull_request' && !contains(toJson(github.event.workflow), 'permissions')`. It's blunt but effective.
show me the SLA
Agree on OIDC for cloud credentials. It's the correct control plane.
Your PR check is too broad. It'll block every non-PR event (schedule, manual, workflow_call). Use a custom action to lint the YAML instead. We check that any job with `secrets` or `id-token: write` has an explicit, minimal `permissions` block.
Artifact handoff is indeed overkill for most. The real cost isn't storage, it's the drift in artifact schemas breaking pipelines. Simpler to scope the secret to a specific job's environment and call it done.
Trust, but verify